runtime:process
Host process information: environment, arguments, working directory, platform, signals, and exit. Aligned in spirit with the WinterTC CLI-API proposal.
Environment access is gated on Env; the signal exports on Signals. Exposed as an ES module under the runtime: scheme. Status: Available.
Import
import { env, args, platform, arch, cwd, exit, unmask } from "runtime:process"; // Or the default aggregate: import process from "runtime:process";
Exports
| Export | Type | Description |
|---|---|---|
env | Record<string, string | Secret> | Environment variables as a mutable in-process object, seeded from a host snapshot taken when the module is evaluated (plus any --env-file values). Reads, writes, and deletes work in-process; they do not propagate to the host or to child processes. Secret-keyed values are Secret wrappers (see below). |
args | readonly string[] | Program arguments after the runtime binary and the script (or -e snippet). Frozen. Excludes the executable and script path. |
platform | string | Host operating system — the OS-native std value ("linux" | "macos" | "windows"). |
arch | string | Host CPU architecture — the OS-native std value ("x86_64" | "aarch64" | "arm"). |
cwd() | () => string | Current working directory. A function (not a value) because it can change during a run. |
exit(code = 0) | (code?: number) => never | Records the exit code and halts execution immediately — code after the call does not run. The embedder treats it as a clean exit, not an error. |
unmask(value) | (value: string | Secret) => string | Reveal the real value of a masked Secret. A plain string is returned unchanged, so unmask(env.ANY) is always safe. |
Secret | class | Opaque holder for a masked env value. Renders as "[redacted]" in console, string coercion, and JSON. Call unmask() to read it. |
signals() | () => SignalName[] | Signal names this platform can deliver. Signals. |
onSignal(sig, fn) | (SignalName, (SignalName) => void) => void | Run fn when sig arrives, suppressing its default action. Signals. |
offSignal(sig, fn) | (SignalName, (SignalName) => void) => void | Remove a handler; removing the last one restores the default action. Signals. |
permissions | object | What this run may reach (see below). No capability. |
env — reading and writing
import { env } from "runtime:process"; console.log(env.HOME); // read env.FEATURE_FLAG = "on"; // write (in-process only) delete env.SECRET; // delete (in-process only)
args — program arguments
// esrun app.js build --watch import { args } from "runtime:process"; console.log(args); // ["build", "--watch"]
signals — graceful shutdown
Watching a signal suppresses its default action, which is what lets a SIGTERM drain instead of kill.
import { onSignal, offSignal } from "runtime:process"; const shutdown = async (signal) => { offSignal(signal, shutdown); // a second ^C should kill, not queue await server.stop(); }; onSignal("SIGINT", shutdown); onSignal("SIGTERM", shutdown);
| Platform | Deliverable |
|---|---|
| Unix | SIGINT, SIGTERM, SIGHUP, SIGUSR1, SIGUSR2 |
| Windows | SIGINT, SIGBREAK |
While anything is watched the program stays running to receive it, as in Node and Deno; removing the last handler releases it. Repeated deliveries coalesce.
exit — stopping the run
import { exit } from "runtime:process"; if (failed) exit(1); // records the code and halts immediately exit(); // defaults to 0
permissions — what this run may reach
Fixed at launch by esrun's denial flags, so has() is a synchronous boolean: there is nothing to request and no prompt to await.
import { permissions } from "runtime:process"; permissions.denied; // ["read", "write"] — [] when nothing is denied permissions.has("net"); // true if (permissions.has("write")) await fs.write("cache.json", data);
| Export | Type | Description |
|---|---|---|
permissions.denied | readonly PermissionName[] | Names this run may not use, in capability order. |
permissions.has(name) | (PermissionName) => boolean | Whether name is available. Throws TypeError for any other name, and for a second argument. |
PermissionName is "read", "write", "imports", "net", "listen", "env", "run", "signals" — the same words the --deny-<name> flags use. Needs no capability, so it answers under --deny-all too.
A scoped grant reports true: --allow-env=PORT grants the env capability and narrows what it yields, so has("env") means "you may read an environment variable", not "you may read this one". The same holds for --allow-net=<hosts> and the rest.
There is no per-value form — has("read", "/etc/passwd") throws. Which paths and hosts are allowed is set by the deployment, not asked about by the application, and the exact answer for one value is to make the call and catch ERR_PERMISSION_DENIED. A path is judged after the runtime resolves it, so an answer given in advance could be stale by the time the call happens.
Secrets — masked by default
Secret-bearing keys are exposed as a Secret that renders as [redacted] in console, strings, and JSON. A key qualifies if it ends in _KEY, _TOKEN, _SECRET, _PASS(WORD), or contains CREDENTIAL/AUTH.
import { env, unmask } from "runtime:process"; // Keys ending in *_SECRET(S) / *_PASSWORD(S) are masked by default. console.log(env.DB_PASSWORD); // [redacted] console.log(`${env.DB_PASSWORD}`); // [redacted] JSON.stringify(env); // ..."DB_PASSWORD":"[redacted]"... const pw = unmask(env.DB_PASSWORD); // real value, explicit unmask(env.DB_HOST); // plain strings pass through
Call unmask(value) for the real string; plain values pass through unchanged.
Masking stops accidental log leaks — code you run can still call unmask itself.
Load values from a file with esrun --env-file=.env (no auto-discovery; the OS environment wins unless --env-override is passed).
Errors
| Error | When |
|---|---|
TypeError | unmask() gets something other than a string or a Secret from env. |
DOMException | name "NotAllowedError" — the Env capability is not granted (env access), or Signals (signal access). |
TypeError | onSignal() gets a name this runtime does not know. |
Error | onSignal() gets a signal this platform cannot deliver — better than a handler that never fires. |
The default export is an object bundling all named exports — useful for a single import binding — but named imports are preferred for clarity and tree-shaking.