Skip to content

Exec reference

The cross-platform subprocess helper, and the seam that makes it fakeable.

For using it, see the subprocess seam.

const realExec: ExecFn;

Runs a command and resolves with its result. Never rejects for a process-level failure — a command that cannot start, times out, or exits nonzero all resolve with the relevant field set.

type ExecFn = (cmd: string[], opts?: ExecOptions) => Promise<ExecResult>;

The injection seam. realExec satisfies it, and so does any fake you write. Accept one in your own functions and default it to realExec.

interface ExecOptions {
cwd?: string;
timeoutMs?: number;
env?: Record<string, string | undefined>;
input?: string;
}
Option Default Notes
cwd the current process’s
timeoutMs 60000 On expiry the promise settles with timedOut: true
env inherited Keys you omit are inherited. A value of undefined unsets the variable rather than setting it empty
input none Piped to stdin, then the stream is closed. EPIPE is swallowed
interface ExecResult {
code: number | null;
stdout: string;
stderr: string;
timedOut: boolean;
spawnError?: string;
}
Field Meaning
code Exit code, or null if the process was terminated by a signal
stdout / stderr Captured output, UTF-8 decoded
timedOut true when timeoutMs elapsed
spawnError Present when the process could not start at all — the command was not found, or was not executable

Check spawnError first, then timedOut, then code.

  • argv array, no shell. shell: true is never set, so there are no quoting hazards and no injection surface. Shell syntax such as | or > is not interpreted. npm shims still resolve on Windows.
  • UTF-8 across chunk boundaries. Both pipes use setEncoding("utf8"), so a multi-byte character split across two reads decodes correctly rather than becoming replacement characters.
  • The timeout settles on the timer itself, not on the child’s close event. A POSIX child that ignores SIGTERM cannot hang the caller.
  • stdin is closed after input is written, so a child waiting on end-of-input proceeds.
Where Default
realExec (opts.timeoutMs) 60000 ms
ClaudeCliProvider constructor 180000 ms

The provider passes its own value through to realExec, so the 60-second default applies only when you call realExec directly.

src/exec.ts and src/providers/types.ts. Behavior is pinned in test/unit/exec.test.ts, including a 50,000-character multi-byte stdin round-trip, an unset-via-undefined case, and a SIGTERM-ignoring child.