bash
Execute a shell command in the session workspace, with optional PTY or background-job handling.
Source
- Entry:
packages/coding-agent/src/tools/bash.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/bash.md - Key collaborators:
packages/coding-agent/src/tools/bash-interactive.ts— PTY/TUI execution path.packages/coding-agent/src/tools/bash-interceptor.ts— blocks tool-better shell patterns.packages/coding-agent/src/tools/bash-skill-urls.ts— expands internal URLs to paths.packages/coding-agent/src/exec/bash-executor.ts— non-PTY shell execution.packages/coding-agent/src/session/streaming-output.ts— tail buffer, truncation, artifact spill.packages/coding-agent/src/tools/tool-timeouts.ts— timeout clamp bounds.packages/coding-agent/src/config/settings-schema.ts— default interceptor rules.docs/bash-tool-runtime.md— deeper executor/runtime notes; use as the companion doc for shell-session internals.
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
command |
string |
Yes | Shell command text to execute. A leading cd <path> && ... is rewritten into cwd only when cwd was omitted. |
env |
Record<string, string> |
No | Extra environment variables. Keys must match ^[A-Za-z_][A-Za-z0-9_]*$ or the tool throws. Values go through internal-URL expansion and are passed as environment values, not shell text. |
timeout |
number |
No | Timeout in seconds. Default 300; clamped to 1..3600 by clampTimeout("bash", ...). |
cwd |
string |
No | Working directory, resolved against session.cwd via resolveToCwd. Must exist and be a directory. |
pty |
boolean |
No | Request PTY mode. Default false. PTY is used only when pty: true, PI_NO_PTY !== "1", and the tool context has a UI. |
async |
boolean |
No | Background execution request. Present only when async.enabled is true for the session. Returns immediately with a job id instead of waiting. |
Outputs
The tool returns a single text content block plus optional details.
- Success, foreground:
content[0].text: command output, or(no output)when the command produced nothing.details.timeoutSeconds: effective timeout after clamping.details.requestedTimeoutSeconds: present when the requested timeout differed from the effective timeout.details.wallTimeMs: elapsed wall-clock milliseconds for completed local/client-terminal runs.details.terminalId: present when execution was routed through a client terminal bridge.details.exitCode: present when the command completed with a non-zero exit code.details.meta.truncation: present when output was truncated in memory; includesartifactIdwhen full output spilled to an artifact.- non-zero exits return a tool result marked
isErrorwith output plusCommand exited with code <n>; they are not thrown.
- Success, background start (
async: trueor auto-background):content[0].text: optional preview tail, timeout notice if any, thenBackground job <id> started: <label>with follow-up instructions.details.async:{ state: "running", jobId, type: "bash" }.
- Background progress / completion:
- delivered through
onUpdate/ async job manager, not the initial return. - running updates contain tail text and
details.async.state: "running"only after the job is considered backgrounded. - completion/failure updates carry final text and
details.async.state: "completed" | "failed". A non-zero exit is recorded as a failed background job.
- delivered through
- Failure:
- unfinished execution (
cancelled, timeout, missing exit status), validation failures, and intercepted commands throwToolError/ToolAbortError.
- unfinished execution (
Stdout and stderr are merged before the model sees them. Definite non-zero exit codes are appended to the returned error result text as Command exited with code <n>.
Flow
BashTool.execute()inpackages/coding-agent/src/tools/bash.tsreadscommand, normalizesenv, and defaultstimeoutto300.- If
cwdis absent, it rewrites a leadingcd <path> && ...into the structuredcwdfield and strips that prefix fromcommand. - If
async: trueis requested whileasync.enabledis off, it throwsToolErrorbefore any execution. - If
bashInterceptor.enabledis on,checkBashInterception()runs against both the original command and thecd-stripped command. A matching enabled rule throws before URL expansion or execution. expandInternalUrls()rewrites supported internal URLs insidecommand, eachenvvalue, and protocol-lookingcwdvalues. Command replacements are shell-escaped;envandcwdreplacements use raw filesystem/string values because they are not interpolated into shell text.resolveToCwd()resolvescwdagainstsession.cwd;fs.stat()verifies that the target exists and is a directory.clampTimeout("bash", requestedTimeoutSec)enforcesTOOL_TIMEOUTS.bash(default: 300,min: 1,max: 3600). When clamped,#buildCompletedResult()/#buildBackgroundStartResult()append a notice line.- Execution path splits:
async: true->#startManagedBashJob()registers a session async job and returns immediately.- Non-PTY with
bash.autoBackground.enabledand an async job manager -> starts a managed job, waits up tomin(thresholdMs, timeoutMs - 1000), and either returns the completed result or converts the run into a background job. - Non-PTY client-terminal bridge, when the session advertises terminal capability and
ptyis false -> creates a remote terminal, streams/polls current output, and releases the terminal after completion. - Otherwise runs foreground execution.
- Foreground non-PTY without client terminal calls
executeBash()frompackages/coding-agent/src/exec/bash-executor.ts. - Foreground PTY calls
runInteractiveBashPty()frompackages/coding-agent/src/tools/bash-interactive.ts. - Local non-PTY and PTY paths allocate an output artifact first when
session.allocateOutputArtifactis available. The artifact path/id are passed into the sink so large output can spill to disk. executeBash()loads shell settings, optional shell snapshot, and shell minimizer settings, then runs via a persistent nativeShellsession or one-shotexecuteShell().docs/bash-tool-runtime.mdcovers that path in detail.runInteractiveBashPty()creates aPtySession, overlays an xterm-backed console UI, forwards user key input into the PTY, captures output throughOutputSink, and kills the PTY on dismiss/dispose.- Client-terminal bridge mode calls
session.getClientBridge().createTerminal(...), emitsterminalIdupdates, polls output until exit/timeout/abort, maps signal exits to137, and releases the handle infinally. - On completion,
#buildCompletedResult()formats(no output)when needed, attaches truncation metadata from the output summary, appends wall-time/timeout/exit notices, and re-checks unfinished status before returning. - On timeout, missing exit status, or cancellation, the tool throws with captured output included when available.
Modes / Variants
- Foreground non-PTY local
- Default path when no client terminal bridge is available.
- Uses
executeBash(). - Streams tail-only updates through
streamTailUpdates()andTailBuffer(DEFAULT_MAX_BYTES).
- Foreground non-PTY client terminal
- Used when
session.getClientBridge()?.capabilities.terminalis true,createTerminalexists, andptyis false. - Streams current terminal output via polling updates with
details.terminalId. - Enforces the same timeout and abort behavior, then releases the terminal handle.
- Used when
- Foreground PTY
- Requires
pty: true, UI context, andPI_NO_PTY !== "1". - Uses
runInteractiveBashPty()and aPtySessionoverlay. - Supports interactive input;
Esckills the session from the overlay.
- Requires
- Explicit background job
- Requires
async: trueandasync.enabled. - Registers a job with
session.asyncJobManagerand returns{ state: "running", jobId }immediately.
- Requires
- Auto-backgrounded non-PTY job
- Requires
bash.autoBackground.enabled, no PTY, and an async job manager. - Starts like a foreground managed job, then backgrounds it when it outlives the wait window.
- Requires
- Intercepted command
- No subprocess created.
- Returns a
ToolErrorpointing the model atread,search,find,edit, orwrite.
Side Effects
- Filesystem
- Validates
cwdwithfs.stat(). - May allocate and write artifact files for full local output (
bash) and minimizer-preserved raw output (bash-original). expandInternalUrls(..., { ensureLocalParentDirs: true })creates parent directories forlocal://paths before execution.
- Validates
- Subprocesses / native bindings / client terminal
- Non-PTY local execution uses native shell execution via
@oh-my-pi/pi-natives(Shell.run()orexecuteShell()). - PTY uses native
PtySession.start(). - Client-terminal mode delegates process execution to the connected client terminal capability.
- Non-PTY local execution uses native shell execution via
- Session state
- Reads session settings for async, auto-background, interceptor, tool availability, and shell configuration.
- Registers jobs with
session.asyncJobManagerfor explicit/auto background runs. - Uses
session.getSessionId()to isolate shell reuse and async session keys. - Uses
session.allocateOutputArtifact()for spill files.
- User-visible prompts / interactive UI
- PTY mode opens a TUI overlay titled
Consoleand forwards input to the PTY. - Background start messages direct the agent to the
jobtool (uselist: truefor a snapshot, or passpoll: [id]to wait).
- PTY mode opens a TUI overlay titled
- Background work / cancellation
- Async and auto-background jobs continue after the initial tool return.
- Cancellation aborts the native run; PTY overlay dismissal also kills the PTY.
Limits & Caps
- Default timeout:
300s(TOOL_TIMEOUTS.bash.defaultinpackages/coding-agent/src/tools/tool-timeouts.ts). - Timeout clamp:
1..3600s(TOOL_TIMEOUTS.bash.min/max). - Auto-background default threshold:
60_000ms(DEFAULT_AUTO_BACKGROUND_THRESHOLD_MSinpackages/coding-agent/src/tools/bash.ts), further capped totimeoutMs - 1000by#resolveAutoBackgroundWaitMs(). - Hard kill grace beyond requested timeout in non-PTY executor:
5_000ms(HARD_TIMEOUT_GRACE_MSinpackages/coding-agent/src/exec/bash-executor.ts). - In-memory output tail cap:
50 * 1024bytes (DEFAULT_MAX_BYTESinpackages/coding-agent/src/session/streaming-output.ts). Once exceeded, the sink keeps only the tail window in memory. - Streaming callback throttle in
executeBash():50msbetweenonChunkcalls when streaming is enabled. - TUI collapsed preview:
10visual lines (BASH_DEFAULT_PREVIEW_LINES) when rendered inline in the agent UI; this is a renderer cap, not a tool output cap.
Errors
- Input validation:
- invalid env key ->
ToolError("Invalid bash env name: <key>"). - async requested while disabled ->
ToolError("Async bash execution is disabled..."). - missing async job manager ->
ToolError("Async job manager unavailable for this session."). - missing/bad
cwd->ToolError("Working directory does not exist: ...")orToolError("Working directory is not a directory: ...").
- invalid env key ->
- Interceptor:
- matched command ->
ToolErrorwithBlocked: <rule.message>and the original command. - invalid interceptor regexes are silently skipped by
compileRules().
- matched command ->
- Internal URL expansion:
- unsupported scheme, unknown skill, path traversal, missing router support, or router resolution failures all throw
ToolErrorfrompackages/coding-agent/src/tools/bash-skill-urls.ts.
- unsupported scheme, unknown skill, path traversal, missing router support, or router resolution failures all throw
- Execution:
- non-zero exit -> returned tool result marked
isError, withdetails.exitCodeand text ending inCommand exited with code <n>. - missing exit code -> thrown
ToolErrorwithCommand failed: missing exit status. - timeout -> thrown
ToolError; PTY/client-terminal modes useCommand timed out after <n> seconds, non-PTY executor returns cancelled output thatBashToolconverts to an error. - user abort ->
ToolAbortErrorwhen the caller signal is aborted.
- non-zero exit -> returned tool result marked
- Artifact allocation / artifact save failures are swallowed in
saveBashOriginalArtifact()andOutputSink.#createFileSink(); execution continues without that artifact.
Notes
strict = trueandconcurrency = "exclusive"are set onBashTool; the tool does not run concurrently with another bash tool call in the same session.commandURL expansions shell-escape replacements;envandcwdexpansion usenoEscape: truebecause they become environment values / filesystem paths, not shell text.checkBashInterception()blocks only when the matching rule'stoolname is present inctx.toolNames; missing tools disable their corresponding rule.- Default interceptor rules come from
DEFAULT_BASH_INTERCEPTOR_RULESinpackages/coding-agent/src/config/settings-schema.ts:cat|head|tail|less|more->readgrep|rg|ripgrep|ag|ack->searchfind|fd|locatewith name/type/glob flags ->findsed -i,perl -i,awk -i inplace->editecho|printf|cat <<with redirection ->write
- PTY mode is ignored in non-UI contexts and when
PI_NO_PTY=1; the tool silently falls back to non-PTY execution. - Non-PTY runs merge
NON_INTERACTIVE_ENVwithenv; PTY runs also prependNON_INTERACTIVE_ENVbefore custom env values. - When the shell minimizer rewrites output inside
executeBash(), the visible output is replaced with minimized text and a[raw output: artifact://<id>]footer may be appended ifonMinimizedSavepersisted the original text. - The TUI renderer parses partial JSON to recover
envassignments early in streaming previews; that behavior is display-only. - For executor internals that are not tool-specific — shell session reuse keys, snapshots, prefix handling, and native timeout behavior — see
docs/bash-tool-runtime.md.