The Bash Tool

Running shell commands, persistent working directory, parallel execution, background jobs, and the sandbox model.

Feature Foundational
9 min read
bash shell background sandbox parallel

What it is#

The Bash tool is Claude Code’s general-purpose escape hatch for anything the dedicated tools cannot do. It runs a single shell command (or a chained pipeline) inside the session’s persistent shell, returns stdout, stderr, and an exit code, and threads the result back into the conversation.

Bash is the only tool that touches the outside world in an open-ended way. Everything else is purpose-built: Read/Edit/Write for files, Grep/Glob for search, WebFetch for URLs. Bash is what you reach for when the task is “run the build”, “git status”, “kill that hung process”, “uv pip install pandas”, “psql -c ‘\dt’”. It is also the tool that costs the most when it goes wrong, which is why it sits behind the permissions system.

A single Bash call is a single command. Multiple independent commands should be issued as multiple parallel Bash calls; commands that depend on each other should be chained with && inside one call.

When to use it#

Use Bash when no specialised tool fits and the operation is naturally a shell command. The common families:

  • Build, test, lint, typecheck. pnpm test, cargo build --release, mypy src/, make.
  • Git operations. git status, git diff, git log --oneline -10, git commit -m "...", gh pr create.
  • Package management. pnpm add foo, uv add bar, cargo add baz.
  • Process management. Starting a dev server in the background, killing a hung process, inspecting ps.
  • One-off data extraction. jq on a JSON file, awk on a CSV, a quick psql query.

Avoid Bash when a dedicated tool is better:

  • Reading files. Use Read. cat works but loses line numbers and the read-before-edit tracking.
  • Editing files. Use Edit. sed -i works but is brittle and bypasses the uniqueness check.
  • Searching code. Use Grep. rg works but Grep is faster to drive and returns better-structured output.
  • Listing many files. Use Glob. find works but Glob is faster.
  • Writing files. Use Write. echo > file works but silently truncates.

The pattern is: prefer the specialised tool because it carries invariants that catch mistakes. Reach for Bash when the operation is genuinely about running something, not about touching a file.

How it works#

The persistent working directory#

The Bash tool maintains shell state across calls in a limited way: the working directory persists. If turn 1 runs cd backend/, turn 2 inherits that directory. However, environment variables, shell functions, and aliases do not persist between calls — each call is a fresh subshell. The safer convention is to use absolute paths and avoid cd entirely, unless the user explicitly asks for a cd.

Parallel calls#

When the model issues two Bash calls in the same turn, the harness runs them concurrently. This is the right pattern when the calls are independent: git status and git diff together take the time of the slower one, not the sum. Calls that depend on each other (run test, then commit) should either be in separate turns or chained with && inside one call.

Background mode#

A Bash call can be sent with run_in_background: true. The harness starts the command, returns control immediately, and notifies the model when the command exits. This is how long-running commands (a build that takes 90 seconds, a test suite that runs for minutes) are kept out of the critical path of the conversation. The output and exit code are delivered later as a notification — the model does not need to poll.

The sandbox model#

Bash runs in the user’s shell on the user’s machine with the user’s permissions. There is no sandbox in the kernel sense — a rm -rf / would do exactly what it says. What gates dangerous commands is the permissions system: allowlists, denylists, and per-command approval prompts configured in settings.json. The default mode prompts the user before running any Bash command not on the allowlist; bypass mode runs everything without prompting; plan mode runs nothing.

Output handling#

Each call returns stdout, stderr, and an exit code. The harness truncates very long output (typically tens of thousands of characters) and shows the model a head and a tail with a truncation marker in between. For commands that produce vast output (a full find /, a verbose build), pipe to a file and Read the file — that preserves your control over how much hits the context window.

Configuration#

Permissions in settings.json#

The relevant configuration is the permissions block. A typical project setup:

{
"permissions": {
"allow": [
"Bash(pnpm test*)",
"Bash(pnpm lint*)",
"Bash(pnpm build*)",
"Bash(git status*)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(gh pr view*)",
"Bash(gh pr list*)",
"Bash(ls*)",
"Bash(cat *)"
],
"deny": [
"Bash(rm -rf /*)",
"Bash(git push --force*)",
"Bash(curl * | sh)"
]
}
}

The allowlist runs without prompting; the denylist refuses outright; anything else triggers an approval prompt. The patterns are glob-shaped, not regex.

Default mode vs accept-edits vs bypass#

ModeWhat Bash does
DefaultPrompts for any command not in the allowlist
Accept-editsSame as default — accept-edits only affects file tools
BypassRuns without prompting (use with care)
PlanRefuses Bash entirely

Timeouts#

A Bash call can specify a timeout (up to 10 minutes). Without one, the harness uses a sensible default. For commands that may genuinely take longer, use background mode rather than a long timeout — background mode does not block the conversation while the command runs.

Examples#

Parallel git inspection#

Three independent reads of the repo state. All three start at once, the slowest one sets the total time.

Terminal window
# Three parallel Bash calls in one turn
git status
git diff --stat
git log --oneline -10

Build chain with short-circuit#

When the steps depend on each other, chain with && so a failure short-circuits.

Terminal window
pnpm lint && pnpm typecheck && pnpm test

If lint fails, typecheck and test never run — which is what you want, because the failure mode is already known.

Background build with notification#

A 90-second build that the model wants out of the critical path.

{
"tool": "Bash",
"command": "pnpm build",
"run_in_background": true,
"description": "Build the site in the background"
}

The model continues the conversation; when the build exits, the harness notifies the model with the result.

Streaming long output to a file#

When find or a verbose build would otherwise blow out context, redirect.

Terminal window
find src/ -name '*.test.ts' > /tmp/test-files.txt
wc -l /tmp/test-files.txt

Then Read the file (paged if needed) to inspect.

One-shot psql#

A quick query against a local database — much cheaper than firing up an MCP server.

Terminal window
psql -d myapp -c "SELECT count(*) FROM users WHERE deleted_at IS NULL;"

Polling — what not to do#

Do not chain a polling Bash after a run_in_background: true call. The harness already delivers a notification when the background command exits. Polling adds latency and burns tokens for nothing.

Terminal window
# BAD: do not do this after run_in_background
until pgrep -f pnpm-build; do sleep 2; done

The correct shape is: fire the background call, continue with whatever does not depend on its output, and respond to the completion notification when it arrives.

Gotchas#

  • cd does not persist environment. Working directory persists; export FOO=bar does not. Use absolute paths and inline env: FOO=bar command.
  • Stdout and stderr are interleaved. The harness collects both but the ordering between them is best-effort. Do not rely on a strict interleave for parsing.
  • Output truncation can hide failures. A test runner that prints thousands of passing lines and one failure at the end can have the failure clipped. Use --reporter=dot or grep for FAIL after the run.
  • Background mode is not for one-off polling. Use it for genuinely long jobs. Short commands (under a few seconds) should run synchronously.
  • cd <dir> && git <cmd> is unnecessary and triggers approval prompts. Git already operates on the working tree; cd first just makes the command harder to allowlist.
  • Quote paths with spaces. "path with spaces/file.txt". Most allowlist patterns assume sane paths; spaces routinely break them.
  • Bash’s exit codes matter to the model. Non-zero exit codes are read as “this failed”. A script that prints “Error: …” and exits 0 will be treated as success. Make exit codes match reality.
  • Long-running interactive commands hang. Anything that prompts (a read, a git rebase -i, a psql without -c) will sit waiting forever. Pass non-interactive flags or pipe input.
  • echo and cat are tempting but wrong for file edits. A cat << EOF > file.txt writes to the file but bypasses the read-before-edit tracking. Use Write instead.

Sync vs background at a glance#

Synchronous Bash
  • Default mode
  • Output returns in the same turn
  • Blocks the conversation until exit
  • Right for fast commands (under ~10s)
  • Right when the model needs the output to plan next steps
Background Bash
  • run_in_background: true
  • Notification on completion
  • Conversation continues in parallel
  • Right for long commands (builds, test suites, deploys)
  • Wrong when the very next step needs the output
Why parallel Bash calls feel like a superpower once you internalise them

A naive workflow runs git status, waits, runs git diff, waits, runs git log, waits. Three round-trips. The model that issues all three in one turn — as parallel Bash calls — gets the same information in one round-trip and uses the saved time to actually think. The pattern generalises: any time you need three independent pieces of information, fire three Bash calls in one message. The latency improvement compounds over a long session.

Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.