All guides
Claude

Bash tool

Checked 09/15/2026View original
On this page

The bash tool is a client tool: Claude doesn't run commands itself. When you include the tool in a request, Claude replies with a tool_use block that names the command to run. Your application runs that command in a bash session it owns and returns the output in a tool_result block.

Your application keeps one bash process alive across tool calls, so state persists between commands. The working directory, environment variables, and any files a command creates are still there for the next command.

The current version of the tool is bash_20250124. For model support, beta headers, and the earlier version, see Tool versions. For all Anthropic-provided tools, see the Tool reference.

Use cases

  • Development workflows: Run build commands, tests, and development tools
  • System automation: Execute scripts, manage files, automate tasks
  • Data processing: Process files, run analysis scripts, manage datasets
  • Environment setup: Install packages, configure environments

Quick start

ant messages create \
  --model claude-opus-5 \
  --max-tokens 1024 \
  --tool '{type: bash_20250124, name: bash}' \
  --message '{role: user, content: List all Python files in the current directory.}'
client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[{"type": "bash_20250124", "name": "bash"}],
    messages=[
        {"role": "user", "content": "List all Python files in the current directory."}
    ],
)

print(response)
const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-opus-5",
  max_tokens: 1024,
  tools: [{ type: "bash_20250124", name: "bash" }],
  messages: [
    {
      role: "user",
      content: "List all Python files in the current directory."
    }
  ]
});

console.log(response);
var client = new AnthropicClient();

var response = await client.Messages.Create(
    new()
    {
        Model = Model.ClaudeOpus5,
        MaxTokens = 1024,
        Tools = [new ToolBash20250124()],
        Messages =
        [
            new()
            {
                Role = Role.User,
                Content = "List all Python files in the current directory.",
            },
        ],
    }
);

Console.WriteLine(response);
client := anthropic.NewClient()

response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5,
	MaxTokens: 1024,
	Tools: []anthropic.ToolUnionParam{
		{OfBashTool20250124: &anthropic.ToolBash20250124Param{}},
	},
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("List all Python files in the current directory.")),
	},
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(response)
import com.anthropic.models.messages.ToolBash20250124;

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    Message response = client.messages().create(
        MessageCreateParams.builder()
            .model(Model.CLAUDE_OPUS_5)
            .maxTokens(1024)
            .addTool(ToolBash20250124.builder().build())
            .addUserMessage("List all Python files in the current directory.")
            .build()
    );

    IO.println(response);
}
use Anthropic\Messages\ToolBash20250124;

$client = new Client();

$response = $client->messages->create(
    model: 'claude-opus-5',
    maxTokens: 1024,
    tools: [new ToolBash20250124()],
    messages: [
        ['role' => 'user', 'content' => 'List all Python files in the current directory.'],
    ],
);

echo $response;
client = Anthropic::Client.new

response = client.messages.create(
  model: "claude-opus-5",
  max_tokens: 1024,
  tools: [{type: "bash_20250124", name: "bash"}],
  messages: [
    {role: "user", content: "List all Python files in the current directory."}
  ]
)

puts response

Claude responds with stop_reason: "tool_use" and a tool_use block that contains the command for your application to run:

{
  "id": "msg_01XAbCDeFgHiJkLmNoPQrStU",
  "model": "claude-opus-5",
  "stop_reason": "tool_use",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "I'll list all Python files in the current directory for you."
    },
    {
      "type": "tool_use",
      "id": "toolu_01A09q90qw90lq917835lq9",
      "name": "bash",
      "input": {
        "command": "ls *.py"
      }
    }
  ]
}

Run input.command in your bash session and send the output back as a tool_result. See Implement the bash tool for the round trip.

How it works

Each tool call is one round trip between Claude and your application:

  1. Claude returns a tool_use block containing the command to run.
  2. Your application runs the command in its bash session.
  3. Your application returns the command's output, stdout and stderr together, to Claude in a tool_result block.
  4. Claude either requests another command in the same session or responds with text.

Claude can also return several tool_use blocks in one response. Run them in order in the same session and return all of the results in one user message. See Parallel tool use.

The API is stateless. Nothing about your shell session travels between requests, so your application decides when the session starts, how long it lives, and when to restart it. For the full request and response cycle, see Handle tool calls.

Parameters

A bash tool definition has two required fields, type and name, and the name must be bash. The tool is schema-less: you don't provide an input_schema, because the schema is built into Claude's model and can't be modified. The following table lists the input fields Claude sets when it calls the tool.

ParameterRequiredDescription
commandYes*The bash command to run
restartNoSet to true to restart the bash session

*Required unless using restart

To handle restart: true, kill the shell process, start a new one, and return a tool_result that confirms the restart. A restarted session starts clean: the working directory, environment variables, and any running processes are gone.

{
  "command": "ls -la *.py"
}

Restart the session:

{
  "restart": true
}

Tool versions

bash_20250124 is the current version of the tool, and it requires no beta header. Every model from Claude Sonnet 3.7 (retired) onward accepts it, including all current Claude models.

The original bash_20241022 version works only with the October 2024 Claude Sonnet 3.5 model (retired). Requests that use it need the anthropic-beta: computer-use-2024-10-22 header, and the SDKs expose it only in their beta namespaces. New integrations should use bash_20250124.

Example: Multistep automation

Claude can chain commands across tool calls to complete a multistep task:

User request:
"Install the requests library and create a simple Python script that
fetches a joke from an API, then run it."

Claude's tool uses:
1. Install package
   {"command": "pip install requests"}

2. Create script
   {"command": "cat > fetch_joke.py << 'EOF'\nimport requests\nresponse = requests.get('https://official-joke-api.appspot.com/random_joke')\njoke = response.json()\nprint(f\"Setup: {joke['setup']}\")\nprint(f\"Punchline: {joke['punchline']}\")\nEOF"}

3. Run script
   {"command": "python fetch_joke.py"}

The session maintains state between commands, so files created in step 2 are available in step 3.

Implement the bash tool

Claude determines which command to run. Your application owns everything else: the shell process, the timeout, and the safety checks. The following steps show a minimal implementation.

<CodeGroup exclude="shell">
  ```python Python
  import subprocess
  import uuid


  class BashSession:
      """A bash process that stays alive between commands so state persists."""

      def __init__(self):
          self.process = subprocess.Popen(
              ["/bin/bash"],
              stdin=subprocess.PIPE,
              stdout=subprocess.PIPE,
              stderr=subprocess.STDOUT,  # interleave errors with output, in order
              start_new_session=True,  # own process group: a timeout can kill every child
              text=True,
          )

      def execute_command(self, command):
          """Run a command in the session and return its output."""
          sentinel = f"__CLAUDE_BASH_DONE_{uuid.uuid4().hex}__"  # unique per call
          self.process.stdin.write(f"{command}\necho {sentinel}\n")
          self.process.stdin.flush()

          output = []
          for line in self.process.stdout:
              if sentinel in line:  # this command's output is complete
                  break
              output.append(line)
          return "".join(output)

      def restart(self):
          self.process.kill()
          self.process.wait()
          self.__init__()


  bash_session = BashSession()
  print(bash_session.execute_command("cd /tmp && pwd"))
  print(bash_session.execute_command("pwd"))  # still /tmp: the session kept its state
  ```

  ```typescript TypeScript
  import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
  import { createInterface, type Interface } from "node:readline";
  import { randomUUID } from "node:crypto";

  // A bash process that stays alive between commands so state persists.
  class BashSession {
    process!: ChildProcessWithoutNullStreams;
    private lines!: Interface;

    constructor() {
      this.start();
    }

    private start(): void {
      this.process = spawn("/bin/bash", {
        detached: true // own process group: a timeout can kill every child
      });
      this.process.stdin.write("exec 2>&1\n"); // interleave errors with output, in order
      this.lines = createInterface({ input: this.process.stdout });
    }

    // Run a command in the session and return its output.
    executeCommand(command: string): Promise<string> {
      const sentinel = `__CLAUDE_BASH_DONE_${randomUUID()}__`; // unique per call
      const output: string[] = [];
      const result = new Promise<string>((resolve) => {
        const onLine = (line: string): void => {
          if (line.includes(sentinel)) {
            // this command's output is complete
            this.lines.off("line", onLine);
            resolve(output.join(""));
          } else {
            output.push(`${line}\n`);
          }
        };
        this.lines.on("line", onLine);
      });
      this.process.stdin.write(`${command}\necho ${sentinel}\n`);
      return result;
    }

    restart(): void {
      this.process.kill("SIGKILL");
      this.lines.close();
      this.start();
    }
  }

  const session = new BashSession();
  console.log(await session.executeCommand("cd /tmp && pwd"));
  console.log(await session.executeCommand("pwd")); // still /tmp: the session kept its state
  session.process.stdin.end(); // closing stdin ends the shell so the script can exit
  ```

  ```csharp C#
  using System.Diagnostics;
  using System.Text;

  var session = new BashSession();
  Console.Write(session.ExecuteCommand("cd /tmp && pwd"));
  Console.Write(session.ExecuteCommand("pwd")); // still /tmp: the session kept its state

  // A bash process that stays alive between commands so state persists.
  class BashSession
  {
      public Process Process { get; private set; }

      public BashSession()
      {
          Process = Start();
      }

      static Process Start()
      {
          var process = Process.Start(new ProcessStartInfo("/bin/bash")
          {
              RedirectStandardInput = true,
              RedirectStandardOutput = true
          })!;
          process.StandardInput.Write("exec 2>&1\n"); // interleave errors with output, in order
          process.StandardInput.Flush();
          return process;
      }

      // Run a command in the session and return its output.
      public string ExecuteCommand(string command)
      {
          var sentinel = $"__CLAUDE_BASH_DONE_{Guid.NewGuid():N}__"; // unique per call
          Process.StandardInput.Write($"{command}\necho {sentinel}\n");
          Process.StandardInput.Flush();

          var output = new StringBuilder();
          while (Process.StandardOutput.ReadLine() is string line)
          {
              if (line.Contains(sentinel)) // this command's output is complete
              {
                  break;
              }
              output.Append(line).Append('\n');
          }
          return output.ToString();
      }

      public void Restart()
      {
          Process.Kill(entireProcessTree: true);
          Process.WaitForExit();
          Process = Start();
      }
  }
  ```

  ```go Go
  import (
  	"bufio"
  	"crypto/rand"
  	"encoding/hex"
  	"fmt"
  	"io"
  	"log"
  	"os/exec"
  	"strings"
  	"syscall"
  )

  // BashSession is a bash process that stays alive between commands so state persists.
  type BashSession struct {
  	cmd    *exec.Cmd
  	stdin  io.WriteCloser
  	output *bufio.Reader
  }

  func NewBashSession() (*BashSession, error) {
  	cmd := exec.Command("/bin/bash")
  	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // own process group: a timeout can kill every child
  	stdin, err := cmd.StdinPipe()
  	if err != nil {
  		return nil, err
  	}
  	stdout, err := cmd.StdoutPipe()
  	if err != nil {
  		return nil, err
  	}
  	cmd.Stderr = cmd.Stdout // interleave errors with output, in order
  	if err := cmd.Start(); err != nil {
  		return nil, err
  	}
  	return &BashSession{cmd: cmd, stdin: stdin, output: bufio.NewReader(stdout)}, nil
  }

  // ExecuteCommand runs a command in the session and returns its output.
  func (s *BashSession) ExecuteCommand(command string) string {
  	buf := make([]byte, 16)
  	rand.Read(buf)
  	sentinel := fmt.Sprintf("__CLAUDE_BASH_DONE_%s__", hex.EncodeToString(buf)) // unique per call
  	fmt.Fprintf(s.stdin, "%s\necho %s\n", command, sentinel)

  	var output strings.Builder
  	for {
  		line, err := s.output.ReadString('\n')
  		if err != nil || strings.Contains(line, sentinel) { // this command's output is complete
  			break
  		}
  		output.WriteString(line)
  	}
  	return output.String()
  }

  // Restart kills the shell and starts a fresh session in its place.
  func (s *BashSession) Restart() error {
  	s.cmd.Process.Kill()
  	s.cmd.Wait()
  	fresh, err := NewBashSession()
  	if err != nil {
  		return err
  	}
  	*s = *fresh
  	return nil
  }

  func main() {
  	session, err := NewBashSession()
  	if err != nil {
  		log.Fatal(err)
  	}
  	fmt.Print(session.ExecuteCommand("cd /tmp && pwd"))
  	fmt.Print(session.ExecuteCommand("pwd")) // still /tmp: the session kept its state
  }
  ```

  ```java Java
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStreamWriter;
  import java.util.UUID;

  // A bash process that stays alive between commands so state persists.
  class BashSession {
      Process process;
      BufferedWriter stdin;
      BufferedReader output;

      BashSession() throws IOException {
          start();
      }

      void start() throws IOException {
          ProcessBuilder builder = new ProcessBuilder("/bin/bash");
          builder.redirectErrorStream(true); // interleave errors with output, in order
          process = builder.start();
          stdin = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
          output = new BufferedReader(new InputStreamReader(process.getInputStream()));
      }

      // Run a command in the session and return its output.
      String executeCommand(String command) throws IOException {
          String sentinel = "__CLAUDE_BASH_DONE_" + UUID.randomUUID() + "__"; // unique per call
          stdin.write(command + "\necho " + sentinel + "\n");
          stdin.flush();

          StringBuilder result = new StringBuilder();
          String line;
          while ((line = output.readLine()) != null) {
              if (line.contains(sentinel)) { // this command's output is complete
                  break;
              }
              result.append(line).append("\n");
          }
          return result.toString();
      }

      void restart() throws IOException, InterruptedException {
          process.destroyForcibly();
          process.waitFor();
          start();
      }
  }

  void main() throws Exception {
      BashSession session = new BashSession();
      IO.println(session.executeCommand("cd /tmp && pwd"));
      IO.println(session.executeCommand("pwd")); // still /tmp: the session kept its state
  }
  ```

  ```php PHP
  // A bash process that stays alive between commands so state persists.
  class BashSession
  {
      public $process;
      public $stdin;
      public $output;

      public function __construct()
      {
          $this->start();
      }

      private function start(): void
      {
          // setsid gives the shell its own process group: a timeout can kill every child
          $this->process = proc_open(
              ['setsid', '/bin/bash'],
              [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['redirect', 1]], // interleave errors with output
              $pipes
          );
          $this->stdin = $pipes[0];
          $this->output = $pipes[1];
      }

      // Run a command in the session and return its output.
      public function executeCommand(string $command): string
      {
          $sentinel = '__CLAUDE_BASH_DONE_' . bin2hex(random_bytes(16)) . '__'; // unique per call
          fwrite($this->stdin, "{$command}\necho {$sentinel}\n");
          fflush($this->stdin);

          $output = '';
          while (($line = fgets($this->output)) !== false) {
              if (str_contains($line, $sentinel)) { // this command's output is complete
                  break;
              }
              $output .= $line;
          }
          return $output;
      }

      public function restart(): void
      {
          proc_terminate($this->process, 9);
          proc_close($this->process);
          $this->start();
      }
  }

  $session = new BashSession();
  echo $session->executeCommand("cd /tmp && pwd");
  echo $session->executeCommand("pwd"); // still /tmp: the session kept its state
  ```

  ```ruby Ruby
  require "open3"
  require "securerandom"

  # A bash process that stays alive between commands so state persists.
  class BashSession
    attr_reader :output, :wait_thread

    def initialize
      start
    end

    # Run a command in the session and return its output.
    def execute_command(command)
      sentinel = "__CLAUDE_BASH_DONE_#{SecureRandom.hex(16)}__" # unique per call
      @stdin.write("#{command}\necho #{sentinel}\n")
      @stdin.flush

      output = +""
      @output.each_line do |line|
        break if line.include?(sentinel) # this command's output is complete

        output << line
      end
      output
    end

    def restart
      Process.kill("KILL", @wait_thread.pid)
      @wait_thread.join
      start
    end

    private

    def start
      # popen2e interleaves errors with output, in order; pgroup gives the shell its
      # own process group so a timeout can kill every child
      @stdin, @output, @wait_thread = Open3.popen2e("/bin/bash", pgroup: true)
    end
  end

  session = BashSession.new
  puts session.execute_command("cd /tmp && pwd")
  puts session.execute_command("pwd") # still /tmp: the session kept its state
  ```
</CodeGroup>

The session interleaves stderr with stdout, so error messages land where they happened. The example leaves out what a complete implementation also needs: a timeout that kills the shell and every process it started when a command hangs, then restarts the session. The [Use command timeouts](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#follow-implementation-best-practices) best practice shows one way to add it.
<CodeGroup exclude="shell">
  ```python Python
  tool_results = []
  for content in response.content:
      if content.type == "tool_use" and content.name == "bash":
          if content.input.get("restart"):
              bash_session.restart()
              result = "Bash session restarted"
          else:
              command = content.input.get("command")
              result = bash_session.execute_command(command)

          # One tool_result per tool_use block, all returned in the next user message
          tool_results.append(
              {"type": "tool_result", "tool_use_id": content.id, "content": result}
          )
  ```

  ```typescript TypeScript
  const toolResults: { type: string; tool_use_id: string; content: string }[] = [];
  for (const block of response.content) {
    if (block.type === "tool_use" && block.name === "bash") {
      let result: string;
      if (block.input.restart) {
        bashSession.restart();
        result = "Bash session restarted";
      } else {
        result = await bashSession.executeCommand(block.input.command ?? "");
      }

      // One tool_result per tool_use block, all returned in the next user message
      toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result });
    }
  }
  ```

  ```csharp C#
  var toolResults = new List<ToolResultBlockParam>();
  foreach (var block in response.Content)
  {
      if (block.TryPickToolUse(out var toolUse) && toolUse.Name == "bash")
      {
          string result;
          if (toolUse.Input.TryGetValue("restart", out var restart) && restart.GetBoolean())
          {
              bashSession.Restart();
              result = "Bash session restarted";
          }
          else
          {
              var command = toolUse.Input["command"].GetString() ?? "";
              result = bashSession.ExecuteCommand(command);
          }

          // One tool_result per tool_use block, all returned in the next user message
          toolResults.Add(new ToolResultBlockParam { ToolUseID = toolUse.ID, Content = result });
      }
  }
  ```

  ```go Go
  var toolResults []anthropic.ContentBlockParamUnion
  for _, block := range response.Content {
  	if block.Type == "tool_use" && block.Name == "bash" {
  		var input struct {
  			Command string `json:"command"`
  			Restart bool   `json:"restart"`
  		}
  		if err := json.Unmarshal(block.Input, &input); err != nil {
  			log.Fatal(err)
  		}

  		var result string
  		if input.Restart {
  			bashSession.Restart()
  			result = "Bash session restarted"
  		} else {
  			result = bashSession.ExecuteCommand(input.Command)
  		}

  		// One tool_result per tool_use block, all returned in the next user message
  		toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false))
  	}
  }
  ```

  ```java Java
  List<Map<String, Object>> toolResults = new ArrayList<>();
  for (ContentBlock block : response.content()) {
      if (block.type().equals("tool_use") && block.name().equals("bash")) {
          String result;
          if (Boolean.TRUE.equals(block.input().get("restart"))) {
              bashSession.restart();
              result = "Bash session restarted";
          } else {
              String command = (String) block.input().get("command");
              result = bashSession.executeCommand(command);
          }

          // One tool_result per tool_use block, all returned in the next user message
          toolResults.add(Map.of("type", "tool_result", "tool_use_id", block.id(), "content", result));
      }
  }
  ```

  ```php PHP
  $toolResults = [];
  foreach ($response->content as $block) {
      if ($block->type === 'tool_use' && $block->name === 'bash') {
          if (!empty($block->input['restart'])) {
              $bashSession->restart();
              $result = 'Bash session restarted';
          } else {
              $result = $bashSession->executeCommand($block->input['command']);
          }

          // One tool_result per tool_use block, all returned in the next user message
          $toolResults[] = ['type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $result];
      }
  }
  ```

  ```ruby Ruby
  tool_results = []
  response.content.each do |block|
    next unless block.type == :tool_use && block.name == "bash"

    result =
      if block.input[:restart]
        bash_session.restart
        "Bash session restarted"
      else
        bash_session.execute_command(block.input[:command])
      end

    # One tool_result per tool_use block, all returned in the next user message
    tool_results << {type: "tool_result", tool_use_id: block.id, content: result}
  end
  ```
</CodeGroup>
<CodeGroup>
  ```bash cURL
  curl https://api.anthropic.com/v1/messages \
    -H "content-type: application/json" \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -d '{
      "model": "claude-opus-5",
      "max_tokens": 1024,
      "tools": [
        {
          "type": "bash_20250124",
          "name": "bash"
        }
      ],
      "messages": [
        {
          "role": "user",
          "content": "List all Python files in the current directory."
        },
        {
          "role": "assistant",
          "content": [
            {
              "type": "tool_use",
              "id": "toolu_01A09q90qw90lq917835lq9",
              "name": "bash",
              "input": {
                "command": "ls *.py"
              }
            }
          ]
        },
        {
          "role": "user",
          "content": [
            {
              "type": "tool_result",
              "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
              "content": "analysis.py\nprocess_data.py\n"
            }
          ]
        }
      ]
    }'
  ```

  ```bash CLI
  ant messages create <<'YAML'
  model: claude-opus-5
  max_tokens: 1024
  tools:
    - type: bash_20250124
      name: bash
  messages:
    - role: user
      content: List all Python files in the current directory.
    - role: assistant
      content:
        - type: tool_use
          id: toolu_01A09q90qw90lq917835lq9
          name: bash
          input:
            command: ls *.py
    - role: user
      content:
        - type: tool_result
          tool_use_id: toolu_01A09q90qw90lq917835lq9
          content: |
            analysis.py
            process_data.py
  YAML
  ```

  ```python Python
  client = anthropic.Anthropic()

  response = client.messages.create(
      model="claude-opus-5",
      max_tokens=1024,
      tools=[{"type": "bash_20250124", "name": "bash"}],
      messages=[
          {"role": "user", "content": "List all Python files in the current directory."},
          {
              "role": "assistant",
              "content": [
                  {
                      "type": "tool_use",
                      "id": "toolu_01A09q90qw90lq917835lq9",
                      "name": "bash",
                      "input": {"command": "ls *.py"},
                  }
              ],
          },
          {
              "role": "user",
              "content": [
                  {
                      "type": "tool_result",
                      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
                      "content": "analysis.py\nprocess_data.py\n",
                  }
              ],
          },
      ],
  )

  print(response.content)
  ```

  ```typescript TypeScript
  const client = new Anthropic();

  const response = await client.messages.create({
    model: "claude-opus-5",
    max_tokens: 1024,
    tools: [{ type: "bash_20250124", name: "bash" }],
    messages: [
      {
        role: "user",
        content: "List all Python files in the current directory."
      },
      {
        role: "assistant",
        content: [
          {
            type: "tool_use",
            id: "toolu_01A09q90qw90lq917835lq9",
            name: "bash",
            input: { command: "ls *.py" }
          }
        ]
      },
      {
        role: "user",
        content: [
          {
            type: "tool_result",
            tool_use_id: "toolu_01A09q90qw90lq917835lq9",
            content: "analysis.py\nprocess_data.py\n"
          }
        ]
      }
    ]
  });

  console.log(response.content);
  ```

  ```csharp C#
  var client = new AnthropicClient();

  var response = await client.Messages.Create(
      new()
      {
          Model = Model.ClaudeOpus5,
          MaxTokens = 1024,
          Tools = [new ToolBash20250124()],
          Messages =
          [
              new()
              {
                  Role = Role.User,
                  Content = "List all Python files in the current directory.",
              },
              new()
              {
                  Role = Role.Assistant,
                  Content = new MessageParamContent(new List<ContentBlockParam>
                  {
                      new ContentBlockParam(new ToolUseBlockParam()
                      {
                          ID = "toolu_01A09q90qw90lq917835lq9",
                          Name = "bash",
                          Input = new Dictionary<string, JsonElement>
                          {
                              ["command"] = JsonSerializer.SerializeToElement("ls *.py"),
                          },
                      }),
                  }),
              },
              new()
              {
                  Role = Role.User,
                  Content = new MessageParamContent(new List<ContentBlockParam>
                  {
                      new ContentBlockParam(new ToolResultBlockParam()
                      {
                          ToolUseID = "toolu_01A09q90qw90lq917835lq9",
                          Content = "analysis.py\nprocess_data.py\n",
                      }),
                  }),
              },
          ],
      }
  );

  Console.WriteLine(response);
  ```

  ```go Go
  client := anthropic.NewClient()

  response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
  	Model:     anthropic.ModelClaudeOpus5,
  	MaxTokens: 1024,
  	Tools: []anthropic.ToolUnionParam{
  		{OfBashTool20250124: &anthropic.ToolBash20250124Param{}},
  	},
  	Messages: []anthropic.MessageParam{
  		anthropic.NewUserMessage(anthropic.NewTextBlock("List all Python files in the current directory.")),
  		anthropic.NewAssistantMessage(
  			anthropic.NewToolUseBlock(
  				"toolu_01A09q90qw90lq917835lq9",
  				map[string]any{"command": "ls *.py"},
  				"bash",
  			),
  		),
  		anthropic.NewUserMessage(
  			anthropic.NewToolResultBlock(
  				"toolu_01A09q90qw90lq917835lq9",
  				"analysis.py\nprocess_data.py\n",
  				false,
  			),
  		),
  	},
  })
  if err != nil {
  	log.Fatal(err)
  }
  fmt.Println(response.Content)
  ```

  ```java Java
  import com.anthropic.core.JsonValue;
  import com.anthropic.models.messages.ContentBlockParam;
  // ...
  import com.anthropic.models.messages.ToolBash20250124;
  import com.anthropic.models.messages.ToolResultBlockParam;
  import com.anthropic.models.messages.ToolUseBlockParam;
  // ...
  void main() {
      AnthropicClient client = AnthropicOkHttpClient.fromEnv();

      MessageCreateParams params = MessageCreateParams.builder()
          .model(Model.CLAUDE_OPUS_5)
          .maxTokens(1024)
          .addTool(ToolBash20250124.builder().build())
          .addUserMessage("List all Python files in the current directory.")
          .addAssistantMessageOfBlockParams(
              List.of(
                  ContentBlockParam.ofToolUse(
                      ToolUseBlockParam.builder()
                          .id("toolu_01A09q90qw90lq917835lq9")
                          .name("bash")
                          .input(
                              ToolUseBlockParam.Input.builder()
                                  .putAdditionalProperty("command", JsonValue.from("ls *.py"))
                                  .build()
                          )
                          .build()
                  )
              )
          )
          .addUserMessageOfBlockParams(
              List.of(
                  ContentBlockParam.ofToolResult(
                      ToolResultBlockParam.builder()
                          .toolUseId("toolu_01A09q90qw90lq917835lq9")
                          .content("analysis.py\nprocess_data.py\n")
                          .build()
                  )
              )
          )
          .build();

      Message response = client.messages().create(params);
      IO.println(response.content());
  }
  ```

  ```php PHP
  use Anthropic\Messages\ToolBash20250124;

  $client = new Client();

  $response = $client->messages->create(
      model: 'claude-opus-5',
      maxTokens: 1024,
      tools: [new ToolBash20250124()],
      messages: [
          ['role' => 'user', 'content' => 'List all Python files in the current directory.'],
          [
              'role' => 'assistant',
              'content' => [
                  [
                      'type' => 'tool_use',
                      'id' => 'toolu_01A09q90qw90lq917835lq9',
                      'name' => 'bash',
                      'input' => ['command' => 'ls *.py'],
                  ],
              ],
          ],
          [
              'role' => 'user',
              'content' => [
                  [
                      'type' => 'tool_result',
                      'tool_use_id' => 'toolu_01A09q90qw90lq917835lq9',
                      'content' => "analysis.py\nprocess_data.py\n",
                  ],
              ],
          ],
      ],
  );

  print_r($response->content);
  ```

  ```ruby Ruby
  client = Anthropic::Client.new

  response = client.messages.create(
    model: "claude-opus-5",
    max_tokens: 1024,
    tools: [{type: "bash_20250124", name: "bash"}],
    messages: [
      {role: "user", content: "List all Python files in the current directory."},
      {
        role: "assistant",
        content: [
          {
            type: "tool_use",
            id: "toolu_01A09q90qw90lq917835lq9",
            name: "bash",
            input: {command: "ls *.py"}
          }
        ]
      },
      {
        role: "user",
        content: [
          {
            type: "tool_result",
            tool_use_id: "toolu_01A09q90qw90lq917835lq9",
            content: "analysis.py\nprocess_data.py\n"
          }
        ]
      }
    ]
  )

  puts response.content
  ```
</CodeGroup>

Repeat the run-and-return cycle while `stop_reason` is `tool_use`. For the full loop, see [Handling results from client tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls#handling-results-from-client-tools).
<CodeGroup exclude="shell">
  ```python Python
  import shlex

  ALLOWED_COMMANDS = {"ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail"}
  SHELL_OPERATORS = {"&&", "||", "|", ";", "&", ">", "<", ">>"}


  def validate_command(command):
      # Allow only commands from an explicit allowlist
      try:
          tokens = shlex.split(command)
      except ValueError:
          return False, "Could not parse command"

      if not tokens:
          return False, "Empty command"

      executable = tokens[0]
      if executable not in ALLOWED_COMMANDS:
          return False, f"Command '{executable}' is not in the allowlist"

      # Reject shell operators written as separate words
      for token in tokens[1:]:
          if token in SHELL_OPERATORS or token.startswith(("$", "`")):
              return False, f"Shell operator '{token}' is not allowed"

      return True, None
  ```

  ```typescript TypeScript
  const ALLOWED_COMMANDS = new Set([
    "ls",
    "cat",
    "echo",
    "pwd",
    "grep",
    "find",
    "wc",
    "head",
    "tail"
  ]);
  const SHELL_OPERATORS = new Set(["&&", "||", "|", ";", "&", ">", "<", ">>"]);

  function validateCommand(command: string): { ok: boolean; reason?: string } {
    // Split on whitespace: enough for a tripwire check
    const tokens = command.split(/\s+/).filter((token) => token.length > 0);
    if (tokens.length === 0) {
      return { ok: false, reason: "Empty command" };
    }

    // Allow only commands from an explicit allowlist
    const executable = tokens[0];
    if (!ALLOWED_COMMANDS.has(executable)) {
      return { ok: false, reason: `Command '${executable}' is not in the allowlist` };
    }

    // Reject shell operators written as separate words
    for (const token of tokens.slice(1)) {
      const bare = token.replace(/^["']+/, ""); // a quoted token can still smuggle an expansion
      if (SHELL_OPERATORS.has(token) || bare.startsWith("$") || bare.startsWith("`")) {
        return { ok: false, reason: `Shell operator '${token}' is not allowed` };
      }
    }

    return { ok: true };
  }
  ```

  ```csharp C#
  var allowedCommands = new HashSet<string>
  {
      "ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail"
  };
  var shellOperators = new HashSet<string> { "&&", "||", "|", ";", "&", ">", "<", ">>" };

  (bool Ok, string? Reason) ValidateCommand(string command)
  {
      // Split on whitespace: enough for a tripwire check
      var tokens = command.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
      if (tokens.Length == 0)
      {
          return (false, "Empty command");
      }

      // Allow only commands from an explicit allowlist
      var executable = tokens[0];
      if (!allowedCommands.Contains(executable))
      {
          return (false, $"Command '{executable}' is not in the allowlist");
      }

      // Reject shell operators written as separate words
      foreach (var token in tokens.Skip(1))
      {
          var bare = token.TrimStart('"', '\''); // a quoted token can still smuggle an expansion
          if (shellOperators.Contains(token) || bare.StartsWith('$') || bare.StartsWith('`'))
          {
              return (false, $"Shell operator '{token}' is not allowed");
          }
      }

      return (true, null);
  }
  ```

  ```go Go
  var allowedCommands = map[string]bool{
  	"ls": true, "cat": true, "echo": true, "pwd": true, "grep": true,
  	"find": true, "wc": true, "head": true, "tail": true,
  }

  var shellOperators = map[string]bool{
  	"&&": true, "||": true, "|": true, ";": true, "&": true,
  	">": true, "<": true, ">>": true,
  }

  func validateCommand(command string) (bool, string) {
  	// Split on whitespace: enough for a tripwire check
  	tokens := strings.Fields(command)
  	if len(tokens) == 0 {
  		return false, "Empty command"
  	}

  	// Allow only commands from an explicit allowlist
  	executable := tokens[0]
  	if !allowedCommands[executable] {
  		return false, fmt.Sprintf("Command %q is not in the allowlist", executable)
  	}

  	// Reject shell operators written as separate words
  	for _, token := range tokens[1:] {
  		bare := strings.TrimLeft(token, `"'`) // a quoted token can still smuggle an expansion
  		if shellOperators[token] || strings.HasPrefix(bare, "$") || strings.HasPrefix(bare, "`") {
  			return false, fmt.Sprintf("Shell operator %q is not allowed", token)
  		}
  	}

  	return true, ""
  }
  ```

  ```java Java
  import java.util.List;
  import java.util.Set;

  static final Set<String> ALLOWED_COMMANDS =
      Set.of("ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail");
  static final Set<String> SHELL_OPERATORS = Set.of("&&", "||", "|", ";", "&", ">", "<", ">>");

  record Validation(boolean ok, String reason) {}

  Validation validateCommand(String command) {
      // Split on whitespace: enough for a tripwire check
      List<String> tokens = List.of(command.trim().split("\\s+"));
      if (tokens.size() == 1 && tokens.get(0).isEmpty()) {
          return new Validation(false, "Empty command");
      }

      // Allow only commands from an explicit allowlist
      String executable = tokens.get(0);
      if (!ALLOWED_COMMANDS.contains(executable)) {
          return new Validation(false, "Command '" + executable + "' is not in the allowlist");
      }

      // Reject shell operators written as separate words
      for (String token : tokens.subList(1, tokens.size())) {
          String bare = token.replaceFirst("^[\"']+", ""); // a quoted token can still smuggle an expansion
          if (SHELL_OPERATORS.contains(token) || bare.startsWith("$") || bare.startsWith("`")) {
              return new Validation(false, "Shell operator '" + token + "' is not allowed");
          }
      }

      return new Validation(true, null);
  }
  ```

  ```php PHP
  const ALLOWED_COMMANDS = ['ls', 'cat', 'echo', 'pwd', 'grep', 'find', 'wc', 'head', 'tail'];
  const SHELL_OPERATORS = ['&&', '||', '|', ';', '&', '>', '<', '>>'];

  function validateCommand(string $command): array
  {
      // Split on whitespace: enough for a tripwire check
      $tokens = preg_split('/\\s+/', trim($command), -1, PREG_SPLIT_NO_EMPTY);
      if ($tokens === false || $tokens === []) {
          return [false, 'Empty command'];
      }

      // Allow only commands from an explicit allowlist
      $executable = $tokens[0];
      if (!in_array($executable, ALLOWED_COMMANDS, true)) {
          return [false, "Command '{$executable}' is not in the allowlist"];
      }

      // Reject shell operators written as separate words
      foreach (array_slice($tokens, 1) as $token) {
          $bare = ltrim($token, '"\''); // a quoted token can still smuggle an expansion
          if (in_array($token, SHELL_OPERATORS, true) || str_starts_with($bare, '$') || str_starts_with($bare, '`')) {
              return [false, "Shell operator '{$token}' is not allowed"];
          }
      }

      return [true, null];
  }
  ```

  ```ruby Ruby
  require "shellwords"

  ALLOWED_COMMANDS = %w[ls cat echo pwd grep find wc head tail].freeze
  SHELL_OPERATORS = ["&&", "||", "|", ";", "&", ">", "<", ">>"].freeze

  def validate_command(command)
    # Allow only commands from an explicit allowlist
    begin
      tokens = Shellwords.split(command)
    rescue ArgumentError
      return [false, "Could not parse command"]
    end

    return [false, "Empty command"] if tokens.empty?

    executable = tokens[0]
    unless ALLOWED_COMMANDS.include?(executable)
      return [false, "Command '#{executable}' is not in the allowlist"]
    end

    # Reject shell operators written as separate words
    tokens[1..].each do |token|
      if SHELL_OPERATORS.include?(token) || token.start_with?("$", "`")
        return [false, "Shell operator '#{token}' is not allowed"]
      end
    end

    [true, nil]
  end
  ```
</CodeGroup>

This check is a tripwire for obvious mistakes, not an enforcement boundary. It rejects the spaced chaining (`&&`), pipes, and redirection that the other examples on this page use. It does not catch an operator glued to a word, such as `cat data.txt|grep x`, because the tokenizer keeps `data.txt|grep` inside one token. Decide which commands and operators your application allows. The real control is isolation: run the whole session inside a container or a virtual machine (see [Security](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#security)).

Handle errors

When a command fails or the session breaks, tell Claude what happened. Return the message as the tool_result content and set is_error to true, which marks the tool call as failed. See Handling errors with is_error.

```json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "Error: command did not finish within 30 seconds",
      "is_error": true
    }
  ]
}
```
```json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "bash: nonexistentcommand: command not found",
      "is_error": true
    }
  ]
}
```
```json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "bash: /root/sensitive-file: Permission denied",
      "is_error": true
    }
  ]
}
```

Follow implementation best practices

<CodeGroup exclude="shell">
  ```python Python
  import concurrent.futures
  import os
  import signal


  def execute_with_timeout(session, command, timeout=30):
      """Run a command in the session, replacing the session if the command hangs."""
      with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
          future = pool.submit(session.execute_command, command)
          try:
              return future.result(timeout=timeout)
          except concurrent.futures.TimeoutError:
              # The group is the shell and every process the command started
              os.killpg(session.process.pid, signal.SIGKILL)
              session.restart()
              return f"Error: command did not finish within {timeout} seconds"
  ```

  ```typescript TypeScript
  // Run a command in the session, replacing the session if the command hangs.
  async function executeWithTimeout(
    session: BashSession,
    command: string,
    timeoutMs = 30000
  ): Promise<string> {
    let timer: NodeJS.Timeout | undefined;
    const timedOut = new Promise<never>((_, reject) => {
      timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
    });
    try {
      return await Promise.race([session.executeCommand(command), timedOut]);
    } catch {
      // The group is the shell and every process the command started
      if (session.process.pid !== undefined) {
        process.kill(-session.process.pid, "SIGKILL");
      }
      session.restart();
      return `Error: command did not finish within ${timeoutMs / 1000} seconds`;
    } finally {
      clearTimeout(timer);
    }
  }
  ```

  ```csharp C#
  using System.Diagnostics;

  // Run a command in the session, replacing the session if the command hangs.
  static string ExecuteWithTimeout(BashSession session, string command, int timeoutSeconds = 30)
  {
      var work = Task.Run(() => session.ExecuteCommand(command));
      if (work.Wait(TimeSpan.FromSeconds(timeoutSeconds)))
      {
          return work.Result;
      }

      // Stop the shell and every process it started, then start a fresh session
      session.Process.Kill(entireProcessTree: true);
      session.Restart();
      return $"Error: command did not finish within {timeoutSeconds} seconds";
  }
  ```

  ```go Go
  // executeWithTimeout runs a command, replacing the session if the command hangs.
  func executeWithTimeout(session *BashSession, command string, timeoutSeconds int) string {
  	done := make(chan string, 1)
  	go func() { done <- session.ExecuteCommand(command) }()

  	select {
  	case result := <-done:
  		return result
  	case <-time.After(time.Duration(timeoutSeconds) * time.Second):
  		// The group is the shell and every process the command started
  		syscall.Kill(-session.cmd.Process.Pid, syscall.SIGKILL)
  		session.Restart()
  		return fmt.Sprintf("Error: command did not finish within %d seconds", timeoutSeconds)
  	}
  }
  ```

  ```java Java
  // Run a command in the session, replacing the session if the command hangs.
  String executeWithTimeout(BashSession session, String command, int timeoutSeconds) throws Exception {
      ExecutorService pool = Executors.newSingleThreadExecutor();
      try {
          Future<String> future = pool.submit(() -> session.executeCommand(command));
          return future.get(timeoutSeconds, TimeUnit.SECONDS);
      } catch (TimeoutException e) {
          // Stop the shell and every process it started, then start a fresh session
          session.process.descendants().forEach(ProcessHandle::destroyForcibly);
          session.process.destroyForcibly();
          session.restart();
          return "Error: command did not finish within " + timeoutSeconds + " seconds";
      } finally {
          pool.shutdownNow();
      }
  }
  ```

  ```php PHP
  // Run a command but give up if it does not finish within the deadline. PHP blocks on
  // pipe reads, so the deadline lives inside the read loop: stream_select() waits for
  // readable output before each fgets() so the loop can check the deadline.
  function executeWithTimeout(BashSession $session, string $command, int $timeout = 30): string
  {
      $sentinel = '__CLAUDE_BASH_DONE_' . bin2hex(random_bytes(16)) . '__'; // unique per call
      fwrite($session->stdin, "{$command}\necho {$sentinel}\n");
      fflush($session->stdin);

      $deadline = microtime(true) + $timeout;
      $output = '';
      while (microtime(true) < $deadline) {
          $read = [$session->output];
          $write = null;
          $except = null;
          if (stream_select($read, $write, $except, 1) === 0) {
              continue; // no output yet; check the deadline again
          }
          $line = fgets($session->output);
          if ($line === false || str_contains($line, $sentinel)) {
              return $output; // this command's output is complete
          }
          $output .= $line;
      }

      // The group is the shell and every process the command started
      posix_kill(-proc_get_status($session->process)['pid'], 9); // 9 = SIGKILL
      $session->restart();
      return "Error: command did not finish within {$timeout} seconds";
  }
  ```

  ```ruby Ruby
  require "timeout"

  # Run a command in the session, replacing the session if the command hangs.
  def execute_with_timeout(session, command, timeout: 30)
    Timeout.timeout(timeout) { session.execute_command(command) }
  rescue Timeout::Error
    # The group is the shell and every process the command started
    Process.kill("KILL", -session.wait_thread.pid)
    session.restart
    "Error: command did not finish within #{timeout} seconds"
  end
  ```
</CodeGroup>

The kill stops the hung command and everything it started. Return the message as an error `tool_result` (see [Handle errors](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#handle-errors)), which marks the tool call as failed.
<CodeGroup exclude="shell">
  ```python Python
  # Commands run in the same session maintain state
  commands = [
      "cd /tmp",
      "echo 'Hello' > test.txt",
      "cat test.txt",  # The session is still in /tmp
  ]
  ```

  ```typescript TypeScript
  // Commands run in the same session maintain state
  const commands = [
    "cd /tmp",
    "echo 'Hello' > test.txt",
    "cat test.txt" // The session is still in /tmp
  ];
  ```

  ```csharp C#
  // Commands run in the same session maintain state
  string[] commands =
  [
      "cd /tmp",
      "echo 'Hello' > test.txt",
      "cat test.txt", // The session is still in /tmp
  ];
  ```

  ```go Go
  // Commands run in the same session maintain state
  commands := []string{
  	"cd /tmp",
  	"echo 'Hello' > test.txt",
  	"cat test.txt", // The session is still in /tmp
  }
  ```

  ```java Java
  // Commands run in the same session maintain state
  List<String> commands = List.of(
      "cd /tmp",
      "echo 'Hello' > test.txt",
      "cat test.txt" // The session is still in /tmp
  );
  ```

  ```php PHP
  // Commands run in the same session maintain state
  $commands = [
      'cd /tmp',
      "echo 'Hello' > test.txt",
      'cat test.txt', // The session is still in /tmp
  ];
  ```

  ```ruby Ruby
  # Commands run in the same session maintain state
  commands = [
    "cd /tmp",
    "echo 'Hello' > test.txt",
    "cat test.txt" # The session is still in /tmp
  ]
  ```
</CodeGroup>
<CodeGroup exclude="shell">
  ```python Python
  def truncate_output(output, max_lines=100):
      lines = output.split("\n")
      if len(lines) > max_lines:
          truncated = "\n".join(lines[:max_lines])
          return f"{truncated}\n\n... Output truncated ({len(lines)} total lines) ..."
      return output
  ```

  ```typescript TypeScript
  function truncateOutput(output: string, maxLines = 100): string {
    const lines = output.split("\n");
    if (lines.length > maxLines) {
      const truncated = lines.slice(0, maxLines).join("\n");
      return `${truncated}\n\n... Output truncated (${lines.length} total lines) ...`;
    }
    return output;
  }
  ```

  ```csharp C#
  string TruncateOutput(string output, int maxLines = 100)
  {
      var lines = output.Split('\n');
      if (lines.Length > maxLines)
      {
          var truncated = string.Join("\n", lines.Take(maxLines));
          return $"{truncated}\n\n... Output truncated ({lines.Length} total lines) ...";
      }
      return output;
  }
  ```

  ```go Go
  func truncateOutput(output string, maxLines int) string {
  	lines := strings.Split(output, "\n")
  	if len(lines) > maxLines {
  		truncated := strings.Join(lines[:maxLines], "\n")
  		return fmt.Sprintf("%s\n\n... Output truncated (%d total lines) ...", truncated, len(lines))
  	}
  	return output
  }
  ```

  ```java Java
  String truncateOutput(String output, int maxLines) {
      String[] lines = output.split("\n", -1);
      if (lines.length > maxLines) {
          String truncated = String.join("\n", Arrays.copyOf(lines, maxLines));
          return truncated + "\n\n... Output truncated (" + lines.length + " total lines) ...";
      }
      return output;
  }
  ```

  ```php PHP
  function truncateOutput(string $output, int $maxLines = 100): string
  {
      $lines = explode("\n", $output);
      if (count($lines) > $maxLines) {
          $truncated = implode("\n", array_slice($lines, 0, $maxLines));
          return "{$truncated}\n\n... Output truncated (" . count($lines) . ' total lines) ...';
      }
      return $output;
  }
  ```

  ```ruby Ruby
  def truncate_output(output, max_lines: 100)
    lines = output.split("\n", -1)
    return output unless lines.length > max_lines

    truncated = lines.first(max_lines).join("\n")
    "#{truncated}\n\n... Output truncated (#{lines.length} total lines) ..."
  end
  ```
</CodeGroup>
<CodeGroup exclude="shell">
  ```python Python
  import logging

  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")


  def execute_and_log(session, command):
      """Run a command in the session and keep an audit record of it."""
      logging.info("command=%r", command)
      output = session.execute_command(command)
      logging.info("output=%r", output[:200])  # first 200 characters
      return output
  ```

  ```typescript TypeScript
  // Run a command in the session and keep an audit record of it.
  async function executeAndLog(session: BashSession, command: string): Promise<string> {
    console.error(`command=${JSON.stringify(command)}`);
    const output = await session.executeCommand(command);
    console.error(`output=${JSON.stringify(output.slice(0, 200))}`); // first 200 characters
    return output;
  }
  ```

  ```csharp C#
  // Run a command in the session and keep an audit record of it.
  static string ExecuteAndLog(BashSession session, string command)
  {
      Console.Error.WriteLine($"command={command}");
      var output = session.ExecuteCommand(command);
      Console.Error.WriteLine($"output={output[..Math.Min(output.Length, 200)]}"); // first 200 characters
      return output;
  }
  ```

  ```go Go
  // executeAndLog runs a command in the session and keeps an audit record of it.
  func executeAndLog(session *BashSession, command string) string {
  	log.Printf("command=%q", command)
  	output := session.ExecuteCommand(command)
  	log.Printf("output=%q", output[:min(len(output), 200)]) // first 200 characters
  	return output
  }
  ```

  ```java Java
  static final Logger AUDIT = Logger.getLogger("bash-audit");

  // Run a command in the session and keep an audit record of it.
  String executeAndLog(BashSession session, String command) throws IOException {
      AUDIT.info("command=" + command);
      String output = session.executeCommand(command);
      AUDIT.info("output=" + output.substring(0, Math.min(output.length(), 200))); // first 200 characters
      return output;
  }
  ```

  ```php PHP
  // Run a command in the session and keep an audit record of it.
  function executeAndLog(BashSession $session, string $command): string
  {
      error_log("command={$command}");
      $output = $session->executeCommand($command);
      error_log('output=' . substr($output, 0, 200)); // first 200 characters
      return $output;
  }
  ```

  ```ruby Ruby
  require "logger"

  AUDIT = Logger.new($stderr)

  # Run a command in the session and keep an audit record of it.
  def execute_and_log(session, command)
    AUDIT.info("command=#{command.inspect}")
    output = session.execute_command(command)
    AUDIT.info("output=#{output[0, 200].inspect}") # first 200 characters
    output
  end
  ```
</CodeGroup>

The records go to `stderr` by default; point them at a file or your logging pipeline to keep them. Include whatever ties the record to the request in your application, such as the end user and the `tool_use_id`.

Security

Beyond isolation, add these controls:

  • Validate commands before running them, with an allowlist rather than a blocklist. See Implement the bash tool.
  • Set resource limits on the shell process (CPU, memory, and disk), for example with ulimit.
  • Log every command and its output so you can audit what ran.
  • Redact credentials and other secrets from output before returning it to Claude.

Pricing

The bash tool definition adds the following input tokens to your request. This is in addition to the per-model tool use system prompt that applies whenever any tool is present.

ModelAdditional input tokens
Claude Opus 5, Claude Opus 4.8, and Claude Opus 4.7325 tokens
Claude Opus 4.6, Claude Sonnet 4.6, and earlier244 tokens

Additional tokens are consumed by:

  • Command outputs (stdout/stderr)
  • Error messages
  • Large file contents

See tool use pricing for complete pricing details.

Common patterns

Development workflows

  • Running tests: pytest && coverage report
  • Building projects: npm install && npm run build
  • Git operations: git status && git add . && git commit -m "message"

For guidance on using git as a checkpoint-and-recovery mechanism in long-running agent workflows, see state management best practices.

File operations

  • Processing data: wc -l *.csv && ls -lh *.csv
  • Searching files: find . -name "*.py" | xargs grep "pattern"
  • Creating backups: tar -czf backup.tar.gz ./data

System tasks

  • Checking resources: df -h && free -m
  • Process management: ps aux | grep python
  • Environment setup: export PATH=$PATH:/new/path && echo $PATH

Limitations

  • No interactive commands: The session can't run vim, less, password prompts, or any command that waits for input on stdin.
  • No GUI applications: The session is command-line only.
  • Session scope: Bash session state is client-side. Your application is responsible for maintaining the shell session between turns.
  • Output limits: The API doesn't truncate tool results (an oversized request is rejected). Truncate large outputs in your application before returning them to Claude.
  • No streaming: Output reaches Claude only when your application returns the tool_result in the next request.

Combining with other tools

The bash tool pairs well with the Text editor tool: Claude edits a file with one tool and requests the command that runs it with the other.

Next steps