# zx Admin & Architecture Guide

This guide is for engineers and architects who need to understand how `zx` is built, how to configure and operate it reliably, how to secure scripts that use it, and how to extend or embed it. For end-user task documentation (installing, writing scripts, the helper API), see the companion **User Guide**.

---

## 1. Executive Summary

`zx` is a Node.js library and CLI for writing shell scripts in JavaScript/TypeScript. It wraps `child_process` in a tagged-template API (`` $`cmd` ``) that automatically escapes interpolated arguments, captures output, and provides ergonomic helpers for piping, retries, parallelism, HTTP, and file operations.

| Attribute | Value |
|-----------|-------|
| Project | `zx` |
| Version | `8.9.0` |
| License | Apache-2.0 |
| Author | Anton Medvedev |
| Publisher | Google (not an officially supported Google product) |
| Language | TypeScript 5 (ESM-first, hybrid CJS/ESM output) |
| Runtimes | Node.js `>= 12.17.0`, Bun `>= 1.0.0`, Deno `1.x`/`2.x`, GraalVM Node.js |
| Platforms | Linux, macOS, Windows |
| Binary | `zx` → `build/cli.js` |
| Homepage | https://google.github.io/zx/ |
| Repository | https://github.com/google/zx |
| Execution engine | [zurk](https://github.com/webpod/zurk) |

**Design philosophy.** `zx` does not replace bash — it augments it with JavaScript's control flow, data handling, exception handling, and parallelism, while delegating actual command execution to a real shell (bash by default). The library is deliberately small and bundled, with a strong emphasis on supply-chain auditability and broad runtime compatibility.

> [!NOTE]
> `zx` is distributed in multiple channels (`latest`, `lite`, `dev`, `legacy`) and through many registries (npm, JSR `@webpod/zx`, GitHub Container Registry, Homebrew). The `lite` channel is the core engine without the CLI or extra helpers.

---

## 2. Architecture

### 2.1 Component overview

```text
                         ┌──────────────────────────────────────┐
   zx CLI (bin)          │  src/cli.ts                           │
   zx script.mjs ──────► │  parseArgv → main() → readScript()    │
                         │            → runScript() (import())    │
                         └───────────────┬──────────────────────┘
                                         │ injects globals, presets
                                         ▼
   import {$} from 'zx'   ┌──────────────────────────────────────┐
   import 'zx/globals' ─► │  src/index.ts (public barrel)         │
                         │   ├── core.ts  ($, ProcessPromise,     │
                         │   │             ProcessOutput, cd,     │
                         │   │             kill, Options)         │
                         │   ├── goods.ts (echo, fetch, question, │
                         │   │             retry, spinner, glob…) │
                         │   ├── error.ts (Fail, exit/errno maps) │
                         │   ├── log.ts   (LogEntry, formatters)  │
                         │   ├── md.ts    (markdown → script)     │
                         │   └── vendor*  (chalk, which, ps, fs,  │
                         │                 yaml, globby, minimist)│
                         └───────────────┬──────────────────────┘
                                         │ exec()
                                         ▼
                         ┌──────────────────────────────────────┐
                         │  zurk/spawn  (execution engine)       │
                         │  → child_process.spawn / spawnSync    │
                         └──────────────────────────────────────┘
```

### 2.2 Source module map (`src/`)

| Module | Responsibility |
|--------|----------------|
| `core.ts` | The heart: the `$` factory, `Options`, `defaults`, `Snapshot`, `ProcessPromise`, `ProcessOutput`, `cd()`, `kill()`, `within()`, `resolveDefaults()`, shell selectors (`useBash`/`usePwsh`/`usePowerShell`), `syncProcessCwd()` |
| `goods.ts` | High-level helpers: `echo`, `fetch`, `question`, `stdin`, `sleep`, `retry`, `expBackoff`, `spinner`, `tempdir`/`tempfile`, `parseArgv`/`updateArgv`/`argv` |
| `cli.ts` | CLI entry point and script preprocessor: argv parsing, `main()`, `readScript()`, `runScript()`, `printUsage()`, `injectGlobalRequire()`, `isMain()` |
| `deps.ts` | Dependency analysis and installation (`parseDeps`, `installDeps`) for the `--install` flag |
| `error.ts` | `Fail` error class; `EXIT_CODES` and `ERRNO_CODES` maps; stack/caller-location parsing; message formatting |
| `log.ts` | `LogEntry` union type, the default `log` function, and per-kind `formatters` (including the `formatCmd` syntax highlighter) |
| `md.ts` | `transformMarkdown()` — converts a Markdown file into a runnable script by extracting `js`/`ts`/`bash` code fences |
| `repl.ts` | `startRepl()` — interactive REPL with history |
| `util.ts` | Generic helpers: `quote`/`quotePowerShell`, `parseDuration`, `toCamelCase`, `parseBool`, `getLines`, `preferLocalBin`, `once`, `proxyOverride`, etc. |
| `internals.ts` | The internal `bus` (a lockable proxy registry used to override bundled dependencies) |
| `globals.ts` / `globals-jsr.ts` | Populate `globalThis` with all exports and declare global types |
| `index.ts` | Public API barrel; re-exports `core` + `goods` + selected vendor packages; defines `VERSION` |
| `vendor-core.ts` / `vendor-extra.ts` / `vendor.ts` | Wrapped re-exports of third-party packages (chalk, which, ps, zurk/spawn, globby, yaml, maml, fs-extra, minimist, envapi, node-fetch-native, depseek, create-require) |
| `versions.ts` | Pinned version strings for `zx` and its bundled dependencies |

### 2.3 The `$` factory and async context

`$` is a `Proxy`-wrapped function backed by an `AsyncLocalStorage<Options>` store. This is what lets configuration be both global and locally scoped:

```ts
const storage = new AsyncLocalStorage<Options>()
const getStore = () => storage.getStore() || defaults

export function within<R>(callback: () => R): R {
  return storage.run({ ...getStore() }, callback)
}
```

- Calling `` $`cmd` `` reads the current store, builds a `Snapshot` of the effective options, and constructs a `ProcessPromise`.
- Calling `$({...})` returns a **new** `$` whose store is merged with the supplied options — this is how presets and chaining work.
- `within()` runs a callback inside a forked store, so mutations to `$` (and `cd()`) inside it do not leak out.

### 2.4 `ProcessPromise` lifecycle

`ProcessPromise extends Promise<ProcessOutput>`. It moves through five stages:

| Stage | Meaning |
|-------|---------|
| `initial` | Constructed but not yet armed |
| `halted` | Created with `{halt: true}`; awaits an explicit `.run()` |
| `running` | Process spawned; output streaming |
| `fulfilled` | Completed with exit code `0` (or `nothrow`) |
| `rejected` | Completed with a non-zero exit code or spawn error |

Three internal gears drive it:

| Method | Role |
|--------|------|
| `build()` | Builds the final `cmd` string from the template pieces and args, applying the `quote` function; validates shell/quote availability and sync constraints |
| `run()` | Calls `zurk`'s `exec()` to spawn the process, wires up `start`/`stdout`/`stderr`/`end` event listeners, and arms the timeout |
| `finalize()` | Constructs the `ProcessOutput`, resolves or rejects the promise based on exit status and `nothrow` |

> [!IMPORTANT]
> `ProcessPromise` instances must be created through `$`, not via `new ProcessPromise()`. Direct construction is disarmed (`disarm()`) and accessing its methods throws *"Inappropriate usage. Apply $ instead of direct instantiation."*

### 2.5 The pipe bus

Piping is implemented with a static `bus` (a `Map` of source→destinations plus a `WeakMap` of promisified streams). Two properties make `zx` piping unusual:

1. **Stream-kind selection** — `pipe.stdout`, `pipe.stderr`, and `pipe.stdall` let you pipe a specific stream.
2. **Replay ("time machine")** — output chunks are buffered, so you can attach a consumer to a process *after* it has already produced output or even settled. The bus replays buffered chunks in order, then forwards live data.

This also enables fan-out (one source → many consumers) and merge (many sources → one consumer via halted processes).

### 2.6 The dependency `bus` and bundling

`internals.ts` exposes a small `bus` that wraps each bundled third-party package in a `Proxy`. Wrapped packages can be overridden at runtime before the bus is **locked** (`index.ts` calls `bus.lock()` on import). This indirection lets `zx` ship a single static bundle while still allowing the `lite` build and tests to swap implementations.

---

## 3. Build & Packaging

`zx` uses a bespoke [esbuild](https://github.com/evanw/esbuild)-based toolchain (the authors documented [why](https://dev.to/antongolub/how-and-why-do-we-bundle-zx-1ca6)). The package is a **hybrid** distribution: it provides both CJS and ESM entry points and bundles its dependencies for reproducibility and auditability.

### 3.1 Entry points

`package.json` `exports` define four public subpaths:

| Subpath | Purpose |
|---------|---------|
| `zx` | Main entry — all features |
| `zx/globals` | Populate `globalThis` with zx functions |
| `zx/cli` | The CLI runner |
| `zx/core` | The template spawner core, for building 3rd-party tools on top of zx |

The binary is declared as `"bin": { "zx": "build/cli.js" }` and the man page as `"man": "./man/zx.1"`.

### 3.2 Build scripts

| npm script | Action |
|------------|--------|
| `build` | Runs versions → js → dts → tests sub-builds |
| `build:js` | esbuild hybrid CJS bundle for `cli, core, deps, globals, index, internals, util, vendor*` |
| `build:vendor` | Bundles `vendor-*.ts` with all deps inlined (`--bundle=all`) |
| `build:dts` | `tsc` then `build-dts.mjs` (merges 3rd-party types via dts-bundle-generator) |
| `build:tests` | Generates export-consistency autotests |
| `build:dcr` | Builds the Docker image (`dcr/Dockerfile`, base `node:24-alpine`) |
| `build:jsr` | Builds JSR publishing assets |
| `build:lite` / `build:pkgjson` / `build:manifest` | Produce the `lite` channel and package manifests |

> [!NOTE]
> Bundling solves four goals at once: smaller install/footprint, reproducible `npx`/`dlx`/`bunx` invocations, support for Node 12–25, and easier auditing (all code in one place).

### 3.3 Runtime dependencies (bundled)

These versions are pinned in `src/versions.ts` and bundled into the published artifacts:

| Package | Version | Role |
|---------|---------|------|
| `zurk` | 0.11.10 | Process execution engine (`exec`, `buildCmd`, `VoidStream`) |
| `chalk` | 5.6.2 | Terminal colors |
| `which` | 7.0.0 | Locate binaries |
| `@webpod/ps` | 1.2.1 | Cross-platform process listing/tree |
| `globby` | 16.2.0 | Glob matching |
| `yaml` | 2.8.4 | YAML parsing |
| `maml` | ^1.0.0 | MAML parsing |
| `fs-extra` | 11.3.5 | Extended fs API |
| `create-require` | 1.1.1 | `require()` shim for ESM |
| `node-fetch-native` | 1.6.7 | `fetch` implementation |
| `depseek` | 0.4.6 | Import scanning for `--install` |
| `minimist` | 1.2.8 | Argument parsing |
| `envapi` | 0.2.3 | `.env` parsing/loading (`dotenv`) |

> [!TIP]
> `versions` is exported at runtime, so an operator can assert the exact bundled dependency versions in a health check: `import { versions } from 'zx'; assert(versions.zx === '8.9.0')`.

---

## 4. Configuration Reference

All configuration lives on the `$` object (and its `defaults`). Options can be set globally (`$.option = …`), per-invocation via a preset (`$({option})`), through CLI flags, or via `ZX_`-prefixed environment variables.

### 4.1 The `Options` interface

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `cwd` | `string` | `process.cwd()` | Working directory for spawned processes |
| `ac` | `AbortController` | auto | Abort controller for the process |
| `signal` | `AbortSignal` | from `ac` | Abort signal |
| `input` | `string \| Buffer \| Readable \| ProcessOutput \| ProcessPromise` | — | stdin to feed the command |
| `timeout` | `Duration` | — | Auto-kill after this duration |
| `timeoutSignal` | `NodeJS.Signals` | `SIGTERM` | Signal used on timeout |
| `stdio` | `StdioOptions` | `'pipe'` | Standard stream configuration |
| `verbose` | `boolean` | `false` | Print commands and their output |
| `sync` | `boolean` | `false` | Synchronous execution |
| `env` | `NodeJS.ProcessEnv` | `process.env` | Environment variables |
| `shell` | `string \| true` | `which bash` | Shell binary (or `true` for default) |
| `nothrow` | `boolean` | `false` | Do not throw on non-zero exit |
| `prefix` | `string` | `set -euo pipefail;` (bash) | Prepended to every command |
| `postfix` | `string` | `; exit $LastExitCode` (pwsh) | Appended to every command |
| `quote` | `typeof quote` | bash `quote` | Argument-escaping function |
| `quiet` | `boolean` | `false` | Suppress all output |
| `detached` | `boolean` | `false` | Spawn detached |
| `preferLocal` | `boolean \| string \| string[]` | `false` | Prefer `node_modules/.bin` binaries |
| `spawn` | `typeof child_process.spawn` | native | Custom spawn implementation |
| `spawnSync` | `typeof child_process.spawnSync` | native | Custom sync spawn implementation |
| `store` | `TSpawnStore` | — | Custom output store |
| `log` | `typeof log` | built-in | Logging function |
| `kill` | `typeof kill` | built-in | Process killer |
| `killSignal` | `NodeJS.Signals` | `SIGTERM` | Default kill signal |
| `halt` | `boolean` | `false` | Create without auto-running |
| `delimiter` | `string \| RegExp` | `/\r?\n/` | Line split delimiter for `lines()`/iteration |

### 4.2 Key configuration knobs

**`$.shell`** — Path to the shell binary. Default is `which bash`. CLI: `--shell=/bin/bash`.

**`$.prefix` / `$.postfix`** — Wrap every command. Default prefix is `set -euo pipefail;` (fail-fast bash). The postfix `; exit $LastExitCode` is used for PowerShell compatibility.

**`$.preferLocal`** — Prepend `node_modules/.bin` to `PATH` so local binaries win over global ones. Accepts a boolean or one/many directories.

**`$.kill`** — The default implements a half-graceful shutdown by walking the process tree via `ps.tree()`. Override for custom logic (e.g. `tree-kill`).

**`$.log`** — A function receiving `LogEntry` objects (`cmd`, `stdout`, `stderr`, `end`, `cd`, `fetch`, `retry`, `kill`, `custom`). Override `$.log.output` to change the stream and `$.log.formatters` to customize rendering. Useful for masking secrets in logged commands.

**`$.delimiter`** — Controls how `lines()` and async iteration split output. Set to `/\0/` for null-delimited tools (`find -print0`).

```ts
import { LogEntry, log } from 'zx/core'

$.log = (entry: LogEntry) => {
  if (entry.kind === 'cmd') process.stderr.write(masker(entry.cmd))
  else log(entry)
}
```

### 4.3 Environment-variable configuration

`resolveDefaults()` reads `process.env` for `ZX_`-prefixed keys and maps the **allowed** subset onto `$`. The allow-list (`ENV_OPTS`) is:

```text
cwd, preferLocal, detached, verbose, quiet, timeout,
timeoutSignal, killSignal, prefix, postfix, shell
```

Keys are camel-cased after stripping the `ZX_` prefix, and values are coerced with `parseBool`. The CLI additionally honors `ZX_`-prefixed forms of all its flags.

| Variable | Effect |
|----------|--------|
| `ZX_VERBOSE`, `ZX_QUIET`, `ZX_SHELL`, `ZX_PREFIX`, `ZX_POSTFIX`, `ZX_CWD`, `ZX_PREFER_LOCAL`, `ZX_TIMEOUT`, … | Set the corresponding `$` option |
| `ZX_REPL_HISTORY` | Path to REPL history (default `~/.zx_repl_history`) |
| `FORCE_COLOR` | Force subprocess colors |
| `CI` | Disables the `spinner()` animation |

---

## 5. Deployment & Operations

### 5.1 Running in containers

The official image is published to the GitHub Container Registry, built `FROM node:24-alpine` (see `dcr/Dockerfile`):

```bash
docker pull ghcr.io/google/zx:8.5.0

# Inline eval
docker run -t ghcr.io/google/zx:8.5.0 -e="await \$({verbose: true})\`echo foo\`"

# Mount a script directory
docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js
```

### 5.2 CI usage

The default GitHub Actions runner ships `npx`, so no install step is needed:

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build with zx
        env:
          FORCE_COLOR: 3
        run: |
          npx zx <<'EOF'
          await $`...`
          EOF
```

> [!TIP]
> Prefer `ZX_`-prefixed env vars in CI (e.g. `ZX_VERBOSE: true`) over editing each script. They apply across every command the runner executes.

### 5.3 Operational checklist

- [ ] Bash (or PowerShell) is available on the target host; on Windows, decide between WSL/Git Bash and `usePowerShell()`/`usePwsh()`.
- [ ] The chosen runtime meets the version floor (Node `>= 12.17.0`, Bun `>= 1.0.0`, Deno `1.x`/`2.x`).
- [ ] For Deno, grant `--allow-read --allow-sys --allow-env --allow-run`.
- [ ] `FORCE_COLOR` set if colored subprocess output is expected.
- [ ] Scripts avoid `process.exit()` immediately after `console.log()` (see Troubleshooting in the User Guide).
- [ ] Timeouts (`$.timeout` / `.timeout()`) set for any network or long-running command.
- [ ] For embedding inside another tool, evaluate `zx@lite` or `zx/core`.

### 5.4 Behavioral defaults to know

- `$.verbose` is **false** by default — successful command output is captured but not echoed; only `stderr` is shown.
- Process cwd synchronization is **off** by default; enable `syncProcessCwd()` if `cd()` must affect `process.cwd()` globally.
- The `spinner()` is disabled under `CI`.
- Sync mode cannot resolve async-interpolated values inside a template.

---

## 6. Security Analysis

`zx` executes shell commands, so it is inherently powerful and must be treated as a privileged tool. The following analysis reflects the actual code paths.

### 6.1 Argument escaping (defense)

The primary safety mechanism is automatic escaping of interpolated values. `quote()` (bash) emits C-style `$'...'` quoting; `quotePowerShell()` emits single-quote-doubled strings. This neutralizes shell metacharacters in user-supplied data:

```js
const name = 'foo & bar'
await $`mkdir ${name}`   // safe; '&' is not interpreted
```

> [!WARNING]
> **Do not defeat the escaper.** Wrapping an interpolation in your own quotes can reintroduce injection. The documented anti-pattern:
> ```ts
> const args = ['param && echo bar']
> const p = $`echo --foo=$'${args}'`
> (await p).stdout // '--foo=$param\nbar\n'  ← command injection
> ```
> Always interpolate a bare `${value}` (or an array of values) and let `zx` quote it.

### 6.2 Command-injection surface via env-driven prefix/postfix

`$.prefix` and `$.postfix` are prepended/appended to **every** command without escaping (they are intended to carry shell directives). Because `ZX_PREFIX` and `ZX_POSTFIX` environment variables map onto these options via `resolveDefaults()`, an attacker who controls the environment can inject arbitrary commands into every invocation (tracked upstream as issue **#1435**, "ZX_PREFIX / ZX_POSTFIX allow implicit global command injection via environment variables").

> [!CAUTION]
> Treat the process environment as a trust boundary. In multi-tenant or untrusted-input contexts, sanitize or strip `ZX_*` variables before running `zx`, and never derive `$.prefix`/`$.postfix` from user input.

### 6.3 Remote and dynamic code execution

- `zx <url>` downloads and **executes** a remote script. Only run trusted URLs.
- `--eval`/`-e` and stdin scripts execute arbitrary code by design.
- `--install` parses imports (via `depseek`) and runs `npm install` for missing packages. The dependency parser inherits `depseek`'s limitations; note the upstream bug where `node:`-prefixed built-in imports were mis-resolved to `node@latest` (issue **#1462**, fixed).

> [!IMPORTANT]
> `--install` shells out to the package manager with `--no-save --no-audit --no-fund`. In hardened environments, prefer committing dependencies explicitly over relying on `--install`, and pin the `--registry`.

### 6.4 Secrets handling

Commands are logged through `$.log` when verbose. To avoid leaking secrets into logs, override the `cmd` formatter to mask sensitive substrings (see [§4.2](#42-key-configuration-knobs)). Avoid passing secrets as command arguments where a child process or `ps` listing could expose them; prefer `$.env` injection.

### 6.5 Supply-chain posture

The project bundles its dependencies into a single auditable artifact, runs `npm audit` checks (`test:audit`), enforces a license check (`test:license`), and runs the [zizmor](https://github.com/woodruffw/zizmor) GitHub Actions security linter (`test:workflow`, configured by `zizmor.yml`). The `lite` channel reduces the audited surface further.

### 6.6 Known security-relevant issues

| Issue | Status | Note |
|-------|--------|------|
| #1435 ZX_PREFIX/ZX_POSTFIX injection | Open | Env-driven prefix/postfix is unescaped by design; sanitize env |
| #1462 `--install` treats `node:` imports as `node@latest` | Closed | Dependency-parse edge case |
| #1443 Unhandled rejection in `fetch().pipe()` on stream error | Open | Wrap piped fetches in error handling |
| #1461 `$` throws synchronously on build failure | Closed | Build-time errors now surface as rejected promises |

---

## 7. Error Handling Internals

The `Fail` class (`src/error.ts`) centralizes error handling:

- **`EXIT_CODES`** — maps shell exit codes to human-readable meanings (e.g. `2` → "Misuse of shell builtins", `127` → "Command not found", `137` → "Kill (terminate immediately)").
- **`ERRNO_CODES`** — maps libc errno values to descriptions (e.g. `-2` → "No such file or directory").
- **`formatExitMessage()` / `formatErrorMessage()` / `formatErrorDetails()`** — build the human-readable `ProcessOutput.message`, including the caller location, exit code annotation, signal, and a sampled extract of error lines.
- **`getCallerLocation()`** — parses the stack (supporting both V8 and JSC stack formats) to attribute the failing command to its source line.

`ProcessOutput extends Error`, so a rejected command is a throwable that also carries `exitCode`, `signal`, `stdout`, `stderr`, and `ok`.

---

## 8. Logging & Observability

The `log.ts` module defines a discriminated-union `LogEntry` and a pluggable `log` function. Each entry has a `kind`:

| Kind | Emitted when | Gated by |
|------|--------------|----------|
| `cmd` | A command starts | verbose |
| `stdout` | stdout data (unless piped) | verbose |
| `stderr` | stderr data | not quiet |
| `end` | A command finishes | verbose |
| `cd` | `cd()` is called | verbose, not quiet |
| `fetch` | `fetch()` is called | verbose, not quiet |
| `retry` | A `retry()` attempt fails | verbose, not quiet |
| `kill` | `kill()` is called | verbose, not quiet |
| `custom` | User-defined | verbose |

The verbose/quiet matrix (only `stderr` is shown by default):

| Event | Verbose | Quiet |
|-------|---------|-------|
| stdout | shown | hidden |
| stderr | shown | hidden |
| cmd | shown | hidden |
| fetch / cd / retry / custom | shown | hidden |

`formatCmd()` provides syntax highlighting for logged commands (greens for the command, yellow for quotes/`$`, red for operators, cyan for reserved words). Output defaults to `process.stderr`; redirect via `$.log.output`.

---

## 9. Testing Strategy

`zx` invests heavily in correctness across runtimes. The `pretest` step builds the bundles first, because tests run against **production artifacts**, not raw source.

| Category | Scripts |
|----------|---------|
| Unit | `test:unit` (`node --experimental-transform-types ./test/all.test.js`) |
| Coverage | `test:coverage` (c8, 98% threshold per the contribution guide) |
| Artifact integrity | `test:npm`, `test:jsr`, `test:dcr` (verify built npm/JSR/Docker artifacts) |
| Size | `test:size` (size-limit, catches bundle duplication) |
| Static analysis | `fmt:check` (prettier), `test:circular` (madge, no circular deps) |
| Types | `test:types` (tsd) |
| Smoke / runtime matrix | `test:smoke:*` for strip-types, tsx, tsc, ts-node, bun, win32, deno, cjs, mjs |
| Supply chain | `test:license` (`test/extra.test.js`), `test:audit`, `test:workflow` (zizmor) |

The full runtime × OS matrix is exercised in CI (`.github/workflows/test.yml`), covering Node 12–25, Bun, Deno, and Windows.

---

## 10. Extending & Embedding

### 10.1 Custom shells

Use `useBash()`, `usePowerShell()`, or `usePwsh()`, or set `$.shell` + `$.quote` manually. These helpers set both the binary and the matching quote function (and bash sets the `set -euo pipefail;` prefix).

### 10.2 Custom spawn / kill

Override `$.spawn`, `$.spawnSync`, and `$.kill` to integrate with alternative process managers or sandboxes. Example: replacing `kill` with `tree-kill`:

```js
import treekill from 'tree-kill'
$.kill = (pid, signal = 'SIGTERM') =>
  new Promise((resolve, reject) =>
    treekill(pid, signal, (err) => (err ? reject(err) : resolve())))
```

### 10.3 Building tools on `zx/core`

Import from `zx/core` to reuse the template spawner without the extra helpers — ideal for libraries that want the `$` ergonomics with their own utility set. The `lite` channel is the packaged form of this approach.

### 10.4 Custom output store and delimiters

Provide `$.store` (a `TSpawnStore`) to capture output differently, and `$.delimiter` to control line splitting for binary-safe tools.

---

## 11. Migration & Compatibility

Breaking changes from v7 → v8, with restoration paths:

| Change | Restore v7 behavior |
|--------|--------------------|
| `$.verbose` defaults to `false` | `$.verbose = true` (or `$.quiet = true` to fully silence) |
| `ssh` API removed | `import { ssh } from 'webpod'` |
| No PowerShell auto-detect on Windows | `usePowerShell()` or `usePwsh()` |
| cwd sync disabled by default | `syncProcessCwd()` |

> [!NOTE]
> v7 is in maintenance mode (bugfixes only). v8 is substantially smaller, faster, and safer. The `--experimental` CLI flag is deprecated and no longer required.

---

## 12. Repository Structure

```text
zx/
├── src/                 Core library (TypeScript)
│   ├── core.ts          $, ProcessPromise, ProcessOutput, Options, cd, kill
│   ├── goods.ts         echo, fetch, question, retry, spinner, glob, temp*
│   ├── cli.ts           CLI entry + script preprocessor
│   ├── deps.ts          --install dependency analysis
│   ├── error.ts         Fail, EXIT_CODES, ERRNO_CODES
│   ├── log.ts           LogEntry, log, formatters
│   ├── md.ts            Markdown → script transform
│   ├── repl.ts          REPL
│   ├── util.ts          quote, parseDuration, helpers
│   ├── internals.ts     dependency bus
│   ├── globals*.ts      global injection + type decls
│   ├── vendor*.ts       wrapped 3rd-party deps
│   ├── versions.ts      pinned dep versions
│   └── index.ts         public API barrel
├── scripts/             esbuild-based build toolchain
├── test/                unit, smoke (bun/deno/win32/cjs/mjs), it (artifact)
├── test-d/              type tests (tsd)
├── examples/            runnable sample scripts
├── man/zx.1             man page
├── dcr/Dockerfile       container image (node:24-alpine)
├── docs/                VitePress documentation site
├── package.json         hybrid exports, bin, build scripts
└── tsconfig.json
```

---

## 13. Quick Reference

### 13.1 Defaults at a glance

| Setting | Default |
|---------|---------|
| Shell | `which bash` |
| Prefix (bash) | `set -euo pipefail;` |
| `verbose` | `false` |
| `quiet` | `false` |
| `nothrow` | `false` |
| `sync` | `false` |
| `detached` | `false` |
| `preferLocal` | `false` |
| `stdio` | `'pipe'` |
| `killSignal` / `timeoutSignal` | `SIGTERM` |
| `delimiter` | `/\r?\n/` |
| cwd sync | disabled |

### 13.2 Entry points

| Import | Use |
|--------|-----|
| `zx` | Everything |
| `zx/globals` | Inject globals |
| `zx/cli` | CLI runner |
| `zx/core` | Spawner core for embedding |

### 13.3 Operational commands

```bash
# Run a script
zx script.mjs

# Verbose via env (CI-friendly)
ZX_VERBOSE=true zx script.mjs

# Pin registry, auto-install deps
zx --install --registry=https://registry.npmjs.org/ script.mjs

# Container
docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js
```

---

_Generated June 2026 from google/zx at b6bc975. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
