MusePi

bash

Execute a shell command in the session workspace, with optional PTY or background-job handling.

Source

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; it does not extend the effective timeout, so jobs are still killed after the clamped 1..3600 second budget.

Outputs

The tool returns a single text content block plus optional details.

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>.

Command policy and dedicated-tool routing

Two independent settings can prevent a Bash subprocess from starting. They serve different purposes and run at different points in the tool-call lifecycle.

Setting Purpose Rule syntax Result when matched
bash.patterns Command-specific execution policy Literal text with * wildcards Allows the call, requests human approval, or denies it.
bashInterceptor.patterns Prefer a dedicated tool over Bash JavaScript regular expression, optional flags, tool name, and message Returns a Bash tool error telling the model to call the named dedicated tool instead.

bash.patterns: permission policy

bash.patterns is for commands that must be allowed, confirmed by a person, or refused regardless of whether another tool could perform the work. Rules are ordered; the first matching rule wins. Each rule has a match glob and an approval value of allow, prompt, or deny.

bash:
  patterns:
    - match: "git *"
      approval: allow
    - match: "curl *"
      approval: prompt
    - match: "rm -rf *"
      approval: deny

Use this setting for safety and user control. It remains useful for commands with no appropriate replacement tool, such as destructive removal, network access, deployment scripts, or project-specific scripts.

bashInterceptor.patterns: dedicated-tool routing

bashInterceptor is an opt-in routing layer (bashInterceptor.enabled defaults to false). It is for commands that are technically valid Bash but are better expressed through an available dedicated tool. Each pattern is a regular expression and includes the name of that replacement tool and the explanation shown to the model.

bashInterceptor:
  enabled: true
  patterns:
    - pattern: '^\s*(cat|head|tail)\s+'
      tool: read
      message: "Use the read tool instead; it handles binary files and provides better context."
    - pattern: '^\s*(grep|rg)\s+'
      tool: grep
      message: "Use the grep tool instead; it respects .gitignore and returns structured results."

An interceptor rule only applies when its tool is available in the current session. If read is disabled, a cat rule targeting read does not block the Bash call. This makes the interceptor a best-effort capability preference rather than an execution-security boundary.

The built-in default rules route common operations such as cat to read, rg to grep, in-place sed to edit, shell redirection to write, and unmanaged services/background processes to hub. See DEFAULT_BASH_INTERCEPTOR_RULES in packages/coding-agent/src/config/settings-schema.ts for the complete list.

For compatibility with existing custom regexes, the interceptor always checks the complete original command first. It then checks raw, flat command fragments separated by unquoted and unescaped &&, ||, ;, |, &, or newlines. It also checks fragments after leading environment assignments are removed:

git add file && git commit -m "message"
GIT_AUTHOR_NAME=Dev git commit -m "message"

An anchored rule such as ^\s*git\s+commit\b can therefore match the git commit command in both examples. A stage that consumes another command’s stdout through an unquoted | or |& (for example grep x in printf 'x\n' | grep x) is not treated as an interception candidate: it reads piped stdin, which the path-based dedicated tools cannot supply, so only a standalone or first-stage command is matched. Blank and comment-only continuation lines after the pipe preserve that context. Quoted, escaped, and commented text is not treated as a command. Heredocs, parameter expansion, command substitution, backticks, grouping, and malformed quoting retain only the complete-command check; the interceptor deliberately does not attempt to become a full shell parser.

Interaction and selection guide

The approval policy is resolved before execution. A matching bash.patterns deny never reaches the interceptor. A matching prompt reaches the interceptor only after the user accepts the approval request. If an accepted call then matches an interceptor rule, the Bash call still does not run; the model receives the routing error and should invoke the dedicated tool.

Avoid configuring the same operation in both places unless that two-step behavior is intended. For example, a prompt rule for cat * plus an enabled cat-to-read interceptor first asks the user to approve Bash, then rejects Bash and asks the model to use read.

Choose the setting by the desired outcome:

Flow

  1. BashTool.execute() in packages/coding-agent/src/tools/bash.ts reads command, normalizes env, and defaults timeout to 300. Commands execute exactly as written — there is no pre-execution rewrite pass.
  2. If cwd is absent, it rewrites a leading cd <path> && ... into the structured cwd field and strips that prefix from command.
  3. If async: true is requested while async.enabled is off, it throws ToolError before any execution.
  4. If bashInterceptor.enabled is on, checkBashInterception() runs against both the original command and the cd-stripped command. For each form, configured regexes still check the complete input first, then each flat command separated by unquoted/unescaped &&, ||, ;, |, |&, &, or newlines (excluding stages that consume piped stdin from | or |&, including across blank/comment continuations), followed by versions of those fragments without leading NAME=value assignments. A matching enabled rule throws before URL expansion or execution.
  5. expandInternalUrls() rewrites supported internal URLs inside command, each env value, and protocol-looking cwd values. Command replacements are shell-escaped; env and cwd replacements use raw filesystem/string values because they are not interpolated into shell text.
  6. resolveToCwd() resolves cwd against session.cwd; fs.stat() verifies that the target exists and is a directory.
  7. clampTimeout("bash", requestedTimeoutSec) enforces TOOL_TIMEOUTS.bash (default: 300, min: 1, max: 3600). When clamped, #buildCompletedResult() / #buildBackgroundStartResult() append a notice line.
  8. Execution path splits:
    1. async: true -> #startManagedBashJob() registers a session async job and returns immediately.
    2. Non-PTY with bash.autoBackground.enabled, an async job manager below its running-job cap, and no client-terminal bridge available (the bridge wins when both apply) -> starts a managed job, waits up to min(thresholdMs, timeoutMs - 1000), and either returns the completed result or converts the run into a background job.
    3. Non-PTY client-terminal bridge, when the session advertises terminal capability and pty is false -> creates a remote terminal, streams/polls current output, and releases the terminal after completion.
    4. Otherwise runs foreground execution.
  9. Foreground non-PTY without client terminal calls executeBash() from packages/coding-agent/src/exec/bash-executor.ts.
  10. Foreground PTY calls runInteractiveBashPty() from packages/coding-agent/src/tools/bash-interactive.ts.
  11. Local non-PTY and PTY paths allocate an output artifact first when session.allocateOutputArtifact is available. The artifact path/id are passed into the sink so large output can spill to disk.
  12. executeBash() loads shell settings, optional shell snapshot, and shell minimizer settings, then runs via a persistent native Shell session or one-shot executeShell(). docs/bash-tool-runtime.md covers that path in detail.
  13. runInteractiveBashPty() creates a PtySession, overlays an xterm-backed console UI, forwards user key input into the PTY, captures output through OutputSink, and kills the PTY on dismiss/dispose.
  14. Client-terminal bridge mode calls session.getClientBridge().createTerminal(...), emits terminalId updates, polls output until exit/timeout/abort, maps signal exits to 137, and releases the handle in finally.
  15. 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.
  16. On timeout, missing exit status, or cancellation, the tool throws with captured output included when available.

Modes / Variants

  1. Foreground non-PTY local
    • Default path when no client terminal bridge is available.
    • Uses executeBash().
    • Streams tail-only updates through streamTailUpdates() and TailBuffer(DEFAULT_MAX_BYTES).
  2. Foreground non-PTY client terminal
    • Used when session.getClientBridge()?.capabilities.terminal is true, createTerminal exists, and pty is false.
    • Streams current terminal output via polling updates with details.terminalId.
    • Enforces the same timeout and abort behavior, then releases the terminal handle.
  3. Foreground PTY
    • Requires pty: true, UI context, and PI_NO_PTY !== "1".
    • Uses runInteractiveBashPty() and a PtySession overlay.
    • Supports interactive input; Esc kills the session from the overlay.
  4. Explicit background job
    • Requires async: true and async.enabled.
    • Registers a job with session.asyncJobManager and returns { state: "running", jobId } immediately.
  5. 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.
  6. Intercepted command
    • No subprocess created.
    • Returns a ToolError pointing the model at read, grep, glob, edit, or write.

Side Effects

Limits & Caps

Errors

Notes