Back to Resources

How to Integrate and Sandbox Claude Bash Tool

Integrate Claude bash tools with container sandboxes, timeout management, output capture, and safe execution loops in Python and TypeScript.

Autonomous coding agents and operations assistants cannot function effectively without the ability to interact directly with the operating system. Whether validating that a dependency installed correctly, executing unit test suites, inspecting system logs, or generating project structures, shell access turns static text generation into active software engineering.

To standardize how models interact with the shell, Anthropic formalized the built-in bash tool (type: "bash_20250124" and type: "bash_20241022"). Rather than forcing developers to construct arbitrary custom function schemas for command execution, Claude natively recognizes and structures bash commands with standard parameter conventions.

However, granting an LLM direct access to a terminal introduces severe operational risks. A hallucinated command, unescaped path variable, or runaway infinite loop can exhaust host resources, overwrite critical system configuration files, or leak production secrets.

In this guide, we walk through the architecture of Anthropic built-in bash tool, explore the threat model of autonomous terminal execution, implement robust sandboxed runners in Python and TypeScript, examine multi-turn cURL exchanges, and configure production-grade security circuit breakers.

Companion code repository: ZeroLabs Claude Recipes Monorepo.

Contents

What Is the Anthropic Built-In Bash Tool Specification?

Unlike custom user-defined tools that require verbose JSON Schema parameter declarations, Anthropic built-in system tools are recognized directly by the Claude 3.7 and Claude 3.5 model families.

The tool definition requires two top-level fields:

json
{  "tools": [    {      "type": "bash_20250124",      "name": "bash"    }  ]}

When Claude decides to invoke the bash tool, it returns a tool_use content block in the assistant response turn. The block specifies name: "bash" and contains an input object with two keys:

  1. command (string, required): The shell command string to execute in the target environment.
  2. restart (boolean, optional): A boolean flag signaling whether the host application should discard the current shell session and instantiate a fresh session environment.

Here is an example of an assistant response turn dispatching a bash tool call:

json
{  "role": "assistant",  "content": [    {      "type": "text",      "text": "I will check the available disk space and memory utilization on the system."    },    {      "type": "tool_use",      "id": "toolu_01948af934bc8120",      "name": "bash",      "input": {        "command": "df -h && free -m"      }    }  ]}

For detailed architectural instructions on defining custom tools and configuring parallel execution, refer to our guides on How to Define and Register Tools with Claude and How to Implement Strict Tool Use and Error Recovery Loops. Official platform documentation is maintained at Anthropic Tool Use Overview.

What Are the Core Risks of Unsandboxed Agent Shell Access?

Connecting Claude directly to a local terminal without safety isolation creates severe attack surfaces:

  1. Host Compromise & Privilege Escalation: If the agent runner executes under root privileges, Claude has the capacity to wipe /etc, alter network routing tables, install rootkits, or corrupt shared mounts.
  2. Infinite Loops & Hanging Processes: Commands like tail -f /var/log/syslog or unmonitored watch loops will block standard input/output channels indefinitely, hanging your API worker threads.
  3. Context Window Exhaustion: Running cat large_dataset.csv or find / can generate millions of characters of raw text. Injecting unbounded stdout directly into the conversation history exceeds Claude max token window and incurs massive token charges.
  4. Fork Bombs & Resource Exhaustion: Recursive shell functions (:(){ :|:& };:) or unmetered compilation jobs can starve CPU cores and trigger Linux Out-Of-Memory (OOM) killer terminations.
  5. Credential Exfiltration: Shell access allows agents to execute curl requests transmitting local .env secrets or SSH keys to third-party servers.

What Are the Five Defense-in-Depth Sandboxing Guardrails?

To mitigate these operational hazards, our production architecture implements five discrete security controls:

Guardrail LayerPrimary MechanismFailure Mode Prevented
1. Filesystem IsolationNon-root user, restricted working directory (/tmp/claude_sandbox)System directory tampering, root overwrite
2. Process Group Timeoutos.killpg(os.getpgid(proc.pid), signal.SIGTERM) followed by SIGKILLZombie processes, hanging socket loops
3. Stream Buffer CappingMax 64 KB stdout/stderr byte captureContext window overflow, memory exhaustion
4. Command ValidationDisallow destructive operators (rm -rf /, raw disk writes, unauthorized network tools)Intentional or accidental host destruction
5. Structured Error Feedbacktool_result with is_error: true and captured stderrBlind failures, unhandled crashes

How Does an Autonomous Bash Execution Loop Flow?

An agentic terminal workflow operates as a stateful, iterative cycle. Claude generates commands, the sandboxed environment validates and executes them, and the runtime returns execution artifacts back to the model.

text
+-------------------------------------------------------------+|                      Claude 3.7 Sonnet                      ||           Dispatches: tool_use (name: "bash")               |+------------------------------+------------------------------+                               |                               v+-------------------------------------------------------------+|                  Sandbox Security Gateway                   ||  - Command Allowlist / Denylist Check                       ||  - Path Traversal & Restricted Directory Enforcement        |+------------------------------+------------------------------+                               |                               v+-------------------------------------------------------------+|               Isolated Subprocess Execution                 ||  - Dedicated Process Group (setsid)                         ||  - Hard Timeout Timer (e.g. 20s)                            ||  - Output Stream Buffering (max 64 KB)                      |+------------------------------+------------------------------+                               |                               v+-------------------------------------------------------------+|                  Tool Result Construction                   ||  - Status Check (exit_code == 0)                            ||  - Format: stdout + stderr + exit code metadata             ||  - Injects: tool_result (is_error: exit_code != 0)          |+------------------------------+------------------------------+                               |                               +------------------------------+                                                              |                                                              v                                              Back to Claude for next turn

When combining bash tool capabilities with graphical desktop control, review How to Provision and Sandbox Claude Computer Use.

How to Implement the Sandboxed Bash Tool in Python?

Below is our production-ready Python implementation. It utilizes subprocess.Popen with process group isolation (preexec_fn=os.setsid), deterministic timeout handling, stream truncation, and error recovery.

python
"""Sandboxed Claude Bash Tool Runner in Python.Location: recipes/claude/19-integrate-sandbox-bash-tool/python/sandboxed_bash_tool.py"""import osimport sysimport signalimport subprocessimport shutilfrom typing import Any, Dict, List, Optional, Tuplefrom anthropic import AnthropicBASH_TOOL_DEFINITION = {    "type": "bash_20250124",    "name": "bash"}MAX_EXECUTION_TIMEOUT_SECONDS = 20MAX_OUTPUT_BYTES = 65536DISALLOWED_COMMAND_PATTERNS = [    "rm -rf /",    ":(){ :|:& };:",    "mkfs",    "dd if=",    "> /dev/sda",    "chmod -R 777 /"]class SandboxedBashRunner:    """Manages isolated shell execution within a bounded directory."""    def __init__(self, workspace_dir: str = "/tmp/claude_sandbox_py"):        self.workspace_dir = os.path.abspath(workspace_dir)        os.makedirs(self.workspace_dir, exist_ok=True)    def validate_command(self, command: str) -> None:        """Checks command against dangerous blacklisted patterns."""        cmd_stripped = command.strip().lower()        for pattern in DISALLOWED_COMMAND_PATTERNS:            if pattern in cmd_stripped:                raise ValueError(f"Security Policy Violation: command contains blocked pattern \"{pattern}\"")    def execute(self, command: str, timeout: int = MAX_EXECUTION_TIMEOUT_SECONDS) -> Tuple[int, str, bool]:        """        Executes a shell command inside a sandboxed process group.        Returns: (exit_code, output_text, is_error)        """        self.validate_command(command)        proc = subprocess.Popen(            command,            shell=True,            cwd=self.workspace_dir,            stdout=subprocess.PIPE,            stderr=subprocess.PIPE,            text=True,            preexec_fn=os.setsid        )        try:            stdout_data, stderr_data = proc.communicate(timeout=timeout)            exit_code = proc.returncode        except subprocess.TimeoutExpired:            try:                pgid = os.getpgid(proc.pid)                os.killpg(pgid, signal.SIGTERM)                proc.communicate(timeout=2)            except Exception:                try:                    os.killpg(pgid, signal.SIGKILL)                except Exception:                    pass            return -1, f"Error: Command timed out after {timeout} seconds.", True        combined_output = ""        if stdout_data:            combined_output += stdout_data        if stderr_data:            if combined_output:                combined_output += "\n--- STDERR ---\n"            combined_output += stderr_data        if len(combined_output.encode("utf-8")) > MAX_OUTPUT_BYTES:            combined_output = combined_output[:MAX_OUTPUT_BYTES] + "\n[Output truncated: exceeded 64 KB limit]"        is_error = exit_code != 0        if not combined_output.strip():            combined_output = f"(Command executed with exit code {exit_code}, no output produced)"        return exit_code, combined_output, is_errordef run_claude_bash_session(prompt: str) -> None:    """Runs an autonomous conversation loop with Claude and the sandboxed bash runner."""    client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])    runner = SandboxedBashRunner()    messages: List[Dict[str, Any]] = [        {"role": "user", "content": prompt}    ]    print(f"User Prompt: {prompt}")    for turn in range(5):        print(f"\n--- Turn {turn + 1} ---")        response = client.messages.create(            model="claude-3-7-sonnet-20250219",            max_tokens=2048,            tools=[BASH_TOOL_DEFINITION],            messages=messages        )        assistant_content = response.content        messages.append({"role": "assistant", "content": assistant_content})        tool_use_blocks = [b for b in assistant_content if b.type == "tool_use" and b.name == "bash"]        for block in assistant_content:            if block.type == "text":                print(f"Claude: {block.text}")        if not tool_use_blocks:            print("Claude completed task without further tool invocations.")            break        tool_results = []        for tool_call in tool_use_blocks:            command_str = tool_call.input.get("command", "")            print(f"Executing Sandbox Command: {command_str}")            exit_code, output, is_error = runner.execute(command_str)            print(f"Result (code={exit_code}, error={is_error}):\n{output}")            tool_results.append({                "type": "tool_result",                "tool_use_id": tool_call.id,                "content": output,                "is_error": is_error            })        messages.append({"role": "user", "content": tool_results})if __name__ == "__main__":    if "ANTHROPIC_API_KEY" not in os.environ:        print("Error: ANTHROPIC_API_KEY environment variable is required.")        sys.exit(1)    run_claude_bash_session("Check current directory contents, create a file named test.txt with Hello World, and count the lines.")

How to Implement the Sandboxed Bash Tool in TypeScript?

Here is the equivalent implementation in TypeScript using Node.js child process streams and the @anthropic-ai/sdk.

typescript
/** * Sandboxed Claude Bash Tool Runner in TypeScript. * Location: recipes/claude/19-integrate-sandbox-bash-tool/typescript/src/index.ts */import { exec, spawn } from "child_process";import * as fs from "fs";import * as path from "path";import Anthropic from "@anthropic-ai/sdk";const BASH_TOOL_DEFINITION: Anthropic.Tool = {  type: "bash_20250124",  name: "bash"};interface ExecutionResult {  exitCode: number;  output: string;  isError: boolean;}export class SandboxedBashRunner {  private workingDirectory: string;  private timeoutMs: number;  private maxOutputBytes: number;  constructor(options?: {    workingDirectory?: string;    timeoutMs?: number;    maxOutputBytes?: number;  }) {    this.workingDirectory = options?.workingDirectory || "/tmp/claude_sandbox_ts";    this.timeoutMs = options?.timeoutMs || 20000;    this.maxOutputBytes = options?.maxOutputBytes || 65536;    if (!fs.existsSync(this.workingDirectory)) {      fs.mkdirSync(this.workingDirectory, { recursive: true });    }  }  public async execute(command: string): Promise {    if (command.includes("rm -rf /") || command.includes(":(){ :|:& };:")) {      return {        exitCode: 1,        output: "Security Violation: Blocked destructive command pattern.",        isError: true      };    }    return new Promise((resolve) => {      let stdoutAcc = "";      let stderrAcc = "";      let isTimedOut = false;      const proc = spawn("bash", ["-c", command], {        cwd: this.workingDirectory,        detached: true,        env: {          PATH: "/usr/local/bin:/usr/bin:/bin",          HOME: this.workingDirectory,          USER: "sandbox"        }      });      const timer = setTimeout(() => {        isTimedOut = true;        try {          if (proc.pid) {            process.kill(-proc.pid, "SIGKILL");          }        } catch {          // Process already terminated        }        resolve({          exitCode: -1,          output: `Execution timed out after ${this.timeoutMs / 1000}s.`,          isError: true        });      }, this.timeoutMs);      proc.stdout.on("data", (chunk: Buffer) => {        if (stdoutAcc.length  {        if (stderrAcc.length  {        if (isTimedOut) return;        clearTimeout(timer);        const exitCode = code ?? 0;        let output = stdoutAcc;        if (stderrAcc) {          output += (output ? "\n--- STDERR ---\n" : "") + stderrAcc;        }        if (output.length > this.maxOutputBytes) {          output = output.slice(0, this.maxOutputBytes) + "\n[Output truncated: 64 KB limit exceeded]";        }        if (!output.trim()) {          output = `(Command finished with exit code ${exitCode})`;        }        resolve({          exitCode,          output,          isError: exitCode !== 0        });      });      proc.on("error", (err) => {        if (isTimedOut) return;        clearTimeout(timer);        resolve({          exitCode: 1,          output: `Process execution error: ${err.message}`,          isError: true        });      });    });  }}async function runSession() {  const apiKey = process.env.ANTHROPIC_API_KEY;  if (!apiKey) {    console.error("ANTHROPIC_API_KEY is required.");    process.exit(1);  }  const client = new Anthropic({ apiKey });  const runner = new SandboxedBashRunner();  const messages: Anthropic.MessageParam[] = [    { role: "user", content: "List disk storage and create a status report in report.txt." }  ];  for (let turn = 0; turn  b.type === "tool_use" && b.name === "bash"    );    for (const block of response.content) {      if (block.type === "text") console.log(`Claude: ${block.text}`);    }    if (toolUseBlocks.length === 0) {      console.log("Task completed.");      break;    }    const toolResults: Anthropic.ToolResultBlockParam[] = [];    for (const tool of toolUseBlocks) {      const cmd = (tool.input as { command: string }).command;      console.log(`Running: ${cmd}`);      const res = await runner.execute(cmd);      toolResults.push({        type: "tool_result",        tool_use_id: tool.id,        content: res.output,        is_error: res.isError      });    }    messages.push({ role: "user", content: toolResults });  }}runSession().catch(console.error);

How to Execute Multi-Turn Bash Tool Exchanges with cURL?

You can test bash tool behavior without an SDK using raw HTTP requests.

Turn 1: Present Tool Definition to Claude

Terminalbash
curl -s -X POST https://api.anthropic.com/v1/messages \  -H "x-api-key: $ANTHROPIC_API_KEY" \  -H "anthropic-version: 2023-06-01" \  -H "content-type: application/json" \  -d '{    "model": "claude-3-7-sonnet-20250219",    "max_tokens": 1024,    "tools": [      {        "type": "bash_20250124",        "name": "bash"      }    ],    "messages": [      {        "role": "user",        "content": "Check system uptime using bash."      }    ]  }'

Claude responds with a tool_use invocation:

json
{  "id": "msg_01AbCdEfGhIjKlMn",  "type": "message",  "role": "assistant",  "model": "claude-3-7-sonnet-20250219",  "stop_reason": "tool_use",  "content": [    {      "type": "text",      "text": "I will execute the uptime command to inspect system load."    },    {      "type": "tool_use",      "id": "toolu_019a8b7c6d5e4f3a",      "name": "bash",      "input": {        "command": "uptime"      }    }  ]}

Turn 2: Feed Sandboxed Output Back to Claude

Execute the command locally in your sandbox:

Terminalbash
UPTIME_OUTPUT=$(uptime)

Return the output in a tool_result content block:

Terminalbash
curl -s -X POST https://api.anthropic.com/v1/messages \  -H "x-api-key: $ANTHROPIC_API_KEY" \  -H "anthropic-version: 2023-06-01" \  -H "content-type: application/json" \  -d '{    "model": "claude-3-7-sonnet-20250219",    "max_tokens": 1024,    "tools": [      {        "type": "bash_20250124",        "name": "bash"      }    ],    "messages": [      {        "role": "user",        "content": "Check system uptime using bash."      },      {        "role": "assistant",        "content": [          {            "type": "tool_use",            "id": "toolu_019a8b7c6d5e4f3a",            "name": "bash",            "input": {              "command": "uptime"            }          }        ]      },      {        "role": "user",        "content": [          {            "type": "tool_result",            "tool_use_id": "toolu_019a8b7c6d5e4f3a",            "content": " 13:45:10 up 33 days, 15:04, 2 users, load average: 0.15, 0.22, 0.18",            "is_error": false          }        ]      }    ]  }'

Claude interprets the load averages and responds with human-readable text.

What Production Guardrails Prevent Terminal Runaway?

When deploying autonomous agents that interact with shell environments, enforce the following production policies:

  1. Ephemeral Containers: Execute each session inside an ephemeral Docker container or MicroVM (Firecracker) that is destroyed upon session completion.
  2. Read-Only Root Filesystem: Mount root directories as read-only (--read-only), providing only a bounded tmpfs mount for temporary scratch files.
  3. Network Egress Filtering: Restrict outgoing internet connectivity to approved package mirrors (PyPI, npm, GitHub) via an egress HTTP proxy.
  4. Hard Maximum Iteration Limits: Enforce a maximum cap of 10 to 15 tool turns per user prompt to prevent cyclic debugging loops from burning API budget.
  5. Circuit Breakers for Non-Zero Exits: If 3 consecutive bash commands fail with non-zero exit codes, halt the agent and request human operator intervention.
Share