Bash
Feature Definition
Section titled “Feature Definition”The bash tool is the LLM’s interface to the host shell. When an agent needs to run cargo build, git status, or npm install, it emits a structured tool call rather than embedding raw commands in its text output. The tool definition specifies exactly what parameters the model may supply, what permission gate the runtime enforces before execution, and what output format the model receives back.
This is distinct from the process-spawning infrastructure documented in Command Execution and the headless non-interactive exec mode documented in Non-Interactive Exec. This page focuses on the tool schema — the contract between the model and the runtime.
Why It Must Be a Tool
Section titled “Why It Must Be a Tool”Text-based command delegation (telling the model to output shell commands in a fenced code block and then parsing them) creates ambiguity: is this command meant to run, or is it an example? A structured tool call is unambiguous — the runtime knows exactly when the model wants execution. It also enables structured permission gating; you cannot intercept intent reliably from free-form text.
The Hard Parts
Section titled “The Hard Parts”Knowing whether a command is safe before running it requires understanding the command’s semantics. Blocking rm -rf / is obvious, but what about find . -name '*.bak' -delete? Or git clean -fd? Approval systems that ask about every command are unusable; systems that approve everything are dangerous. The solution is intent-learning: map commands to short prefix rules and remember approvals.
See Also
Section titled “See Also”- Approval Flow for host confirmation UI and decision caching.
- Sandbox Modes and Platform Isolation for policy-tier and OS-tier enforcement beneath shell execution.
- Questions for model clarification prompts, which are separate from execution approval.
Aider Implementation
Section titled “Aider Implementation”Aider has no bash tool in the model-tool-call sense. Shell execution is handled entirely within the agent loop as agent-initiated procedural logic, not as an LLM-callable tool.
The entry point is run_cmd() in aider/run_cmd.py and sendcmd() in the coder base classes. These are Python functions called by the coder, not capabilities the model invokes by emitting a JSON tool call. The model produces shell commands embedded in its text reply (e.g., fenced bash blocks), and the coder parses that text to extract and run commands.
Consequences:
- No structured parameter schema; shell commands are parsed from text
- No structured approval gate per command; Aider uses a single
confirm_ask()call before running any shell output - No structured output format; stdout/stderr are returned as raw strings injected into the next user message
The /run chat command executes arbitrary shell commands and feeds the output back into the conversation, but this is a user-initiated slash command, not an LLM-callable tool.
Codex Implementation
Section titled “Codex Implementation”Codex exposes two tool variants for shell execution. Both are defined in codex-rs/core/src/tools/spec.rs (commit 4ab44e2c5).
Dual Tool Variants
Section titled “Dual Tool Variants”Tool 1: shell (spec.rs:336–387)
Takes a pre-formatted command array, bypassing shell parsing:
// Parameter schema (JSON-serialized){ "command": ["bash", "-lc", "<command string>"], // Vec<String> "workdir": "<path>", // optional, defaults to CWD "timeout_ms": 10000, // optional, defaults to 10s "sandbox_permissions": "use_default" | "require_escalated", // Only when escalated: "justification": "<why escalated permissions are needed>", "prefix_rule": ["npm", "install"] // short prefix for approval learning}Tool 2: shell_command (spec.rs:389–449)
Takes a raw shell script string and wraps it in the user’s actual login shell before execution:
{ "command": "npm run build", // String, not array "workdir": "<path>", "login": true, // spawn as login shell; defaults true "timeout_ms": 30000, "sandbox_permissions": "use_default" | "require_escalated", "justification": "<reason>", "prefix_rule": ["npm", "run", "build"]}The shell_command handler calls derive_exec_args() to construct the actual argv by reading the user’s $SHELL environment variable and wrapping appropriately (bash -lc, zsh -lc, powershell -Command, etc.) before delegating to the same shell runtime used by shell.
Permission and Approval Gating
Section titled “Permission and Approval Gating”Escalation policy guard (shell.rs:259–272):
if exec_params.sandbox_permissions.requires_escalated_permissions() && !matches!(turn.approval_policy, AskForApproval::OnRequest){ return Err( "reject command — you should not ask for escalated permissions \ unless the user has explicitly granted them".into() );}The model may request escalated sandbox permissions (e.g., network access, writes outside the project directory), but only if the current AskForApproval policy is OnRequest. Requesting escalation under any other policy is a hard error — the tool call fails before any subprocess is spawned.
Safe command detection (shell.rs:109–119):
is_known_safe_command() checks whether the command is read-only (e.g., cat, ls, git log, grep). Read-only commands skip the human-approval queue and run immediately. Commands that mutate state proceed to the approval overlay.
Environment Construction
Section titled “Environment Construction”shell.rs:247–257 shows the environment merge order:
- Start with the executor dependency environment (from
ToolOrchestrator) - Apply the session-level
shell_environment_policy(managed variables) - Apply any explicit per-call overrides from the tool parameters
- Pass the merged environment to the subprocess spawn
The environment is not inherited from the parent process wholesale. This prevents Codex’s API keys and internal variables from leaking into model-invoked processes.
Execution via ToolOrchestrator
Section titled “Execution via ToolOrchestrator”After permission is satisfied, shell.rs:323–335 hands off to ToolOrchestrator::run(), which:
- Re-checks the
AskForApprovalpolicy for the specific command - Enforces the active sandbox (bubblewrap on Linux, Seatbelt on macOS)
- Streams stdout/stderr via
ShellRuntime - Enforces
timeout_msusing a cancellation deadline - Caps output at 1 MiB (from the exec infrastructure) before returning
Output Format
Section titled “Output Format”The tool result is a string containing the combined stdout/stderr. There is no structured envelope — the raw text is injected directly into the conversation as a tool result content block.
OpenCode Implementation
Section titled “OpenCode Implementation”OpenCode exposes a single bash tool defined in packages/opencode/src/tool/bash.ts (commit 7ed44997).
Parameter Schema (Zod)
Section titled “Parameter Schema (Zod)”parameters: z.object({ command: z.string().describe("The command to execute"),
timeout: z.number() .describe("Optional timeout in milliseconds") .optional(),
workdir: z.string() .describe( "The working directory to run the command in. Defaults to the project root." ) .optional(),
description: z.string() .describe( "Clear, concise description of what this command does in 5–10 words. " + "Example: 'Install npm dependencies', 'Run unit tests'" ),})Note the description parameter — the model must supply a human-readable summary of the command. This is rendered in the permission dialog before the user approves, giving a plain-language explanation alongside the raw command string.
BashArity Permission System
Section titled “BashArity Permission System”OpenCode learns command patterns from approvals. The core data structure is BashArity, defined in packages/opencode/src/permission/arity.ts:
// Maps command prefixes to "how many tokens make up the command identity"// e.g., "git" → 2 means "git status" and "git log" are different commands// "npm run" → 3 means "npm run build" and "npm run test" are differentconst BASH_ARITY: Record<string, number> = { "git": 2, "npm": 2, "npm run": 3, "yarn": 2, "docker": 2, "docker compose": 3, "cargo": 2, "make": 2, // ... 100+ entries};When the model runs npm run build:
- The bash AST is parsed with tree-sitter to extract the command’s prefix tokens
BASH_ARITY["npm run"]returns3, so the prefix rule is["npm", "run", "build"]- If the user approves and selects “always allow this pattern”, future calls matching
["npm", "run", "build"]skip the dialog - But
npm run testhas a different third token, so it asks again
For commands not in the arity dictionary, the entire command string is used as the pattern.
Tree-Sitter External Directory Detection
Section titled “Tree-Sitter External Directory Detection”Beyond basic approval, bash.ts:88–155 uses tree-sitter’s bash grammar to walk the command AST and detect operations that read or write outside the project root:
// Commands that take path argumentsconst PATH_COMMANDS = ["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown", "cat"];
// For each command node in the AST:// 1. Identify if it's in PATH_COMMANDS// 2. Resolve its path argument via realpath()// 3. If resolved path is outside Instance.directory:// → issue a separate ExternalDirectory permission requestThis catches cd /tmp && rm -rf something even inside a compound command. Simple string-matching would miss it; AST traversal cannot.
Execution Flow
Section titled “Execution Flow”bash.ts:166 → Parse shell environment from plugins (hooks: shell.env)bash.ts:175 → Resolve workdir (default: Instance.directory)bash.ts:180 → Request bash permission (BashArity-derived pattern)bash.ts:157 → Request external directory permissions (if detected)bash.ts:194 → child_process.spawn(command, [], { stdio: ["ignore","pipe","pipe"], shell: true })bash.ts:199 → Collect stdout/stderr chunks in real timebash.ts:208 → Wire AbortSignal from the tool contextbash.ts:220 → setTimeout() for timeout enforcementbash.ts:225 → await process exit; cleanup timer and abort handlersOutput Truncation
Section titled “Output Truncation”Output is not truncated inside the tool itself. Instead, the tool returns the full string and the caller passes it through Truncate.output() from packages/opencode/src/tool/truncate.ts:
const MAX_LINES = 2000;const MAX_BYTES = 50 * 1024; // 50 KB
// If either limit exceeded:// 1. Write full output to ./data/tool-output/tool_<id>// 2. Return truncated output + message:// "Output was truncated. Full output saved to <path>. Use grep/read to inspect."Metadata vs Agent Output
Section titled “Metadata vs Agent Output”OpenCode distinguishes two representations of the output:
- Metadata (
MAX_METADATA_LENGTH = 30,000bytes): Rendered in the TUI for the human user. Truncated to prevent UI lag on large outputs. - Full output: Passed to the model in the tool result content block. Potentially larger, subject to the
Truncate.output()limits above.
If the command times out or is aborted, an XML comment block is appended:
<!-- Command was killed after 120000ms timeout -->Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons”stdout Discipline Is Non-Negotiable
Section titled “stdout Discipline Is Non-Negotiable”Anything the tool writes to stdout before the subprocess starts becomes part of the model’s tool result. In Codex’s headless exec mode, this is enforced with #![deny(clippy::print_stdout)] at the crate level. In tool implementations, any debug println!() will corrupt the tool result. Use stderr for diagnostics.
Shell Profile Pollution
Section titled “Shell Profile Pollution”Spawning a login shell (bash -lc) sources ~/.bashrc, ~/.bash_profile, and /etc/profile.d/*. These files often echo things, modify $PATH in ways that change command resolution, or start background daemons. Commands that succeed in the user’s interactive shell may behave differently when invoked via -lc. OpenCode’s plugin shell.env hook lets the tool intercept and sanitize the environment before spawn.
Tree-Sitter Parsing Limits
Section titled “Tree-Sitter Parsing Limits”OpenCode’s AST-based external directory detection only works for commands that tree-sitter’s bash grammar can parse. Heredocs, process substitutions, and obscure bash constructs may fail to parse, causing the detection to fall back to the full command string as the pattern. A parse failure is not an error; the tool still runs, but the external directory detection may be incomplete.
Timeout Interaction with Long-Running Processes
Section titled “Timeout Interaction with Long-Running Processes”OpenCode’s 2-minute default timeout (DEFAULT_TIMEOUT = 2 * 60 * 1000) is appropriate for interactive commands but wrong for things like cargo build in a large workspace. The model must supply an explicit timeout parameter for commands it knows will take longer. If the model doesn’t know, it will be killed mid-build and see a truncated stdout that looks like a build failure.
The Escalation Trap
Section titled “The Escalation Trap”Codex’s dual-policy system (sandbox_permissions: require_escalated) lets the model request elevated access. But if the model develops a habit of requesting escalated permissions for everything (because it doesn’t know the exact sandbox boundaries), it will constantly fail under OnApproval policies where escalation is blocked. The model should default to use_default and escalate only when a previous attempt failed with a permission error.
Output Size and Context Pressure
Section titled “Output Size and Context Pressure”Shell commands can produce arbitrarily large output (cat /dev/urandom | head -c 10M). Even with line/byte caps, a command that produces 50 KB of output takes meaningful context window space. Tools should encourage the model to use targeted commands (grep -c, wc -l) rather than capturing full file contents via cat.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Single Tool Variant
Section titled “Single Tool Variant”OpenOxide should expose one bash tool with an optional login flag rather than Codex’s two-variant design. Two variants create prompt engineering confusion — the model must choose between shell and shell_command without clear guidance.
#[derive(Deserialize, JsonSchema)]pub struct BashParams { /// The shell command to execute. pub command: String,
/// Working directory. Defaults to the project root. pub workdir: Option<PathBuf>,
/// Timeout in milliseconds. Defaults to 120_000 (2 minutes). pub timeout_ms: Option<u64>,
/// Spawn as a login shell (sources ~/.bashrc etc). Defaults to true. pub login: Option<bool>,
/// Human-readable description of what this command does. pub description: String,}Permission Architecture
Section titled “Permission Architecture”Implement a BashArityDict similar to OpenCode’s but serialized to TOML so users can extend it:
[bash_arity]"git" = 2"npm" = 2"npm run" = 3"cargo" = 2"docker compose" = 3Pattern approval is stored in ~/.config/openoxide/approvals.toml per-session (not cross-session) unless the user grants a persistent approval.
Tree-Sitter Permission Analysis
Section titled “Tree-Sitter Permission Analysis”Use the tree-sitter crate with tree-sitter-bash for AST-based command analysis. Detect external directory access before spawning. Reject commands targeting paths listed in protected_paths (.git/, .agents/, .codex/) without user override.
Execution
Section titled “Execution”Use tokio::process::Command with kill_on_drop(true). Wire a CancellationToken to the process via SIGTERM → 500ms → SIGKILL. Capture stdout and stderr separately, merge in timestamp order, then apply truncation.
Output Truncation
Section titled “Output Truncation”Hard limits:
MAX_LINES = 2000MAX_BYTES = 50 * 1024
If exceeded, write full output to ~/.local/share/openoxide/tool-output/<tool_id> and append a message pointing the model to use grep or read_file to inspect it.
Crates
Section titled “Crates”[dependencies]tokio = { features = ["process", "time"] }tree-sitter = "0.22"tree-sitter-bash = "0.21"serde = { features = ["derive"] }schemars = "0.8"