Shell Module
The Shell interface gives your agent the ability to run shell commands to completion. v1 ships run only — interactive sessions (stdin, vim, REPLs) are reserved for a future spawn method.
Interface
interface Shell {
run(cmd: string, opts?: RunOptions): Promise<RunResult>;
}
interface RunOptions {
readonly cwd?: string;
readonly env?: Record<string, string>;
readonly signal?: AbortSignal;
readonly timeoutMs?: number;
/** Async callback — providers must await for backpressure. */
readonly onStdout?: (chunk: Uint8Array) => Promise<void>;
readonly onStderr?: (chunk: Uint8Array) => Promise<void>;
}
interface RunResult {
readonly stdout: Uint8Array;
readonly stderr: Uint8Array;
readonly exitCode: number;
readonly durationMs: number;
}Real-time streaming
The onStdout / onStderr callbacks are how providers stream output as it arrives. The contract:
- When callbacks ARE present: the provider streams chunks as they're produced and awaits each callback before continuing (backpressure).
- When callbacks are NOT present: the provider may use a blocking exec call and return everything at once.
Whichever path is taken, result.stdout and result.stderr always contain the FULL accumulated output.
This dual-mode design lets providers like CloudflareSandboxShell switch between execStream (SSE) when callbacks are present and exec (blocking) when not — without callers needing to know which path is in play.
The auto-injected workspace__<name>__run tool always passes callbacks that emit chunks to the agent's event stream. So when an LLM calls workspace__box__run('npm install') in a real-time-capable provider, you get live progress in your agent stream.
signal.aborted semantics
Providers that support signal MUST break their iteration / kill the underlying process when signal.aborted flips to true. The result.exitCode after abort is provider-specific (typically 0 if no exit event was seen, or -1 if the process was killed before exit).
The CloudflareSandboxShell checks signal.aborted at iteration start in its streaming path — chunks in flight when abort fires are not accumulated and the callback is not invoked.
Auto-injected tool
For a workspace named <name> with shell: true:
| Tool | Schema | Returns |
|---|---|---|
workspace__<name>__run | { command: string; cwd?: string; env?: Record<string, string>; timeoutMs?: number } | { stdout: string; stderr: string; exitCode: number; durationMs: number } |
The tool emits workspace_stdout / workspace_stderr events to the agent's event stream as chunks arrive — your downstream consumers (the AI SDK frontend, custom event handlers) see live output.
Capability config
interface ShellCapConfig {
/** Allowlist of command first-tokens. Other commands throw at the tool layer. */
allowedCommands?: readonly string[];
/** Default timeoutMs applied when the tool input doesn't override. */
maxDurationMs?: number;
}allowedCommands enforces a first-token allowlist:
capabilities: {
shell: { allowedCommands: ['ls', 'cat', 'wc', 'grep'] },
}The auto-injected tool checks command.split(/\s+/)[0] against the list before delegating to the provider. Useful for restricting an LLM to a small command vocabulary.
maxDurationMs becomes the default timeoutMs if the tool input doesn't supply one.
Deferred features
spawn— interactive sessions with stdin streaming, PTY support. Reserved for Pattern 3e v2.- stdin — passing data to a running command. Workaround: use
writeFileto a temp path, thencommand < /tmp/path. - PTY — terminal emulation, color codes, vim/nano. Same v2 timeline as spawn.
Provider support matrix
| Provider | shell supported |
|---|---|
| In-Memory | ❌ |
| Local Bash | ✅ (subprocess) |
| Cloudflare Filestore | ❌ |
| Cloudflare Sandbox | ✅ (with real-time streaming via execStream + SSE) |
Source
- Interface:
packages/core/src/workspace/types/modules/shell.ts - Tool injection:
packages/core/src/workspace/tool-injection.ts(search formakeShellTools)