Repository Public
1---2title: Bash tool3url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool4description: Let Claude request shell commands that your application runs in a persistent bash session and returns as tool results.5---67<Note>8 To learn how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention).9</Note>1011The bash tool is a [client tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works): 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.1213Your 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.1415The current version of the tool is `bash_20250124`. For model support, beta headers, and the earlier version, see [Tool versions](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#tool-versions). For all Anthropic-provided tools, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference).1617## Use cases1819* **Development workflows:** Run build commands, tests, and development tools20* **System automation:** Execute scripts, manage files, automate tasks21* **Data processing:** Process files, run analysis scripts, manage datasets22* **Environment setup:** Install packages, configure environments2324## Quick start2526<CodeGroup>27 ```bash cURL28 curl https://api.anthropic.com/v1/messages \29 -H "content-type: application/json" \30 -H "x-api-key: $ANTHROPIC_API_KEY" \31 -H "anthropic-version: 2023-06-01" \32 -d '{33 "model": "claude-opus-5",34 "max_tokens": 1024,35 "tools": [36 {37 "type": "bash_20250124",38 "name": "bash"39 }40 ],41 "messages": [42 {43 "role": "user",44 "content": "List all Python files in the current directory."45 }46 ]47 }'48 ```4950 ```bash CLI51 ant messages create \52 --model claude-opus-5 \53 --max-tokens 1024 \54 --tool '{type: bash_20250124, name: bash}' \55 --message '{role: user, content: List all Python files in the current directory.}'56 ```5758 ```python Python59 client = anthropic.Anthropic()6061 response = client.messages.create(62 model="claude-opus-5",63 max_tokens=1024,64 tools=[{"type": "bash_20250124", "name": "bash"}],65 messages=[66 {"role": "user", "content": "List all Python files in the current directory."}67 ],68 )6970 print(response)71 ```7273 ```typescript TypeScript74 const client = new Anthropic();7576 const response = await client.messages.create({77 model: "claude-opus-5",78 max_tokens: 1024,79 tools: [{ type: "bash_20250124", name: "bash" }],80 messages: [81 {82 role: "user",83 content: "List all Python files in the current directory."84 }85 ]86 });8788 console.log(response);89 ```9091 ```csharp C#92 var client = new AnthropicClient();9394 var response = await client.Messages.Create(95 new()96 {97 Model = Model.ClaudeOpus5,98 MaxTokens = 1024,99 Tools = [new ToolBash20250124()],100 Messages =101 [102 new()103 {104 Role = Role.User,105 Content = "List all Python files in the current directory.",106 },107 ],108 }109 );110111 Console.WriteLine(response);112 ```113114 ```go Go115 client := anthropic.NewClient()116117 response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{118 Model: anthropic.ModelClaudeOpus5,119 MaxTokens: 1024,120 Tools: []anthropic.ToolUnionParam{121 {OfBashTool20250124: &anthropic.ToolBash20250124Param{}},122 },123 Messages: []anthropic.MessageParam{124 anthropic.NewUserMessage(anthropic.NewTextBlock("List all Python files in the current directory.")),125 },126 })127 if err != nil {128 log.Fatal(err)129 }130 fmt.Println(response)131 ```132133 ```java Java134 import com.anthropic.models.messages.ToolBash20250124;135136 void main() {137 AnthropicClient client = AnthropicOkHttpClient.fromEnv();138139 Message response = client.messages().create(140 MessageCreateParams.builder()141 .model(Model.CLAUDE_OPUS_5)142 .maxTokens(1024)143 .addTool(ToolBash20250124.builder().build())144 .addUserMessage("List all Python files in the current directory.")145 .build()146 );147148 IO.println(response);149 }150 ```151152 ```php PHP153 use Anthropic\Messages\ToolBash20250124;154155 $client = new Client();156157 $response = $client->messages->create(158 model: 'claude-opus-5',159 maxTokens: 1024,160 tools: [new ToolBash20250124()],161 messages: [162 ['role' => 'user', 'content' => 'List all Python files in the current directory.'],163 ],164 );165166 echo $response;167 ```168169 ```ruby Ruby170 client = Anthropic::Client.new171172 response = client.messages.create(173 model: "claude-opus-5",174 max_tokens: 1024,175 tools: [{type: "bash_20250124", name: "bash"}],176 messages: [177 {role: "user", content: "List all Python files in the current directory."}178 ]179 )180181 puts response182 ```183</CodeGroup>184185Claude responds with `stop_reason: "tool_use"` and a `tool_use` block that contains the command for your application to run:186187```json Output188{189 "id": "msg_01XAbCDeFgHiJkLmNoPQrStU",190 "model": "claude-opus-5",191 "stop_reason": "tool_use",192 "role": "assistant",193 "content": [194 {195 "type": "text",196 "text": "I'll list all Python files in the current directory for you."197 },198 {199 "type": "tool_use",200 "id": "toolu_01A09q90qw90lq917835lq9",201 "name": "bash",202 "input": {203 "command": "ls *.py"204 }205 }206 ]207}208```209210Run `input.command` in your bash session and send the output back as a `tool_result`. See [Implement the bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#implement-the-bash-tool) for the round trip.211212## How it works213214Each tool call is one round trip between Claude and your application:2152161. Claude returns a `tool_use` block containing the `command` to run.2172. Your application runs the command in its bash session.2183. Your application returns the command's output, stdout and stderr together, to Claude in a `tool_result` block.2194. Claude either requests another command in the same session or responds with text.220221Claude 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](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use).222223The 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](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls).224225## Parameters226227A 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.228229| Parameter | Required | Description |230| --------- | -------- | ----------------------------------------- |231| `command` | Yes\* | The bash command to run |232| `restart` | No | Set to `true` to restart the bash session |233234\*Required unless using `restart`235236To 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.237238<Accordion title="Example usage">239 Run a command:240241 ```json242 {243 "command": "ls -la *.py"244 }245 ```246247 Restart the session:248249 ```json250 {251 "restart": true252 }253 ```254</Accordion>255256## Tool versions257258`bash_20250124` is the current version of the tool, and it requires no beta header. Every model from Claude Sonnet 3.7 ([retired](https://platform.claude.com/docs/en/about-claude/model-deprecations)) onward accepts it, including all current Claude models.259260The original `bash_20241022` version works only with the October 2024 Claude Sonnet 3.5 model ([retired](https://platform.claude.com/docs/en/about-claude/model-deprecations)). 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`.261262## Example: Multistep automation263264Claude can chain commands across tool calls to complete a multistep task:265266```text267User request:268"Install the requests library and create a simple Python script that269fetches a joke from an API, then run it."270271Claude's tool uses:2721. Install package273 {"command": "pip install requests"}2742752. Create script276 {"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"}2772783. Run script279 {"command": "python fetch_joke.py"}280```281282The session maintains state between commands, so files created in step 2 are available in step 3.283284## Implement the bash tool285286Claude 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.287288<Steps>289 <Step title="Create a persistent bash session">290 Start one long-lived bash process and run every command inside it. Because a pipe to a live process never reports end-of-file, the session prints a unique sentinel line after each command to mark where that command's output ends:291292 <CodeGroup exclude="shell">293 ```python Python294 import subprocess295 import uuid296297298 class BashSession:299 """A bash process that stays alive between commands so state persists."""300301 def __init__(self):302 self.process = subprocess.Popen(303 ["/bin/bash"],304 stdin=subprocess.PIPE,305 stdout=subprocess.PIPE,306 stderr=subprocess.STDOUT, # interleave errors with output, in order307 start_new_session=True, # own process group: a timeout can kill every child308 text=True,309 )310311 def execute_command(self, command):312 """Run a command in the session and return its output."""313 sentinel = f"__CLAUDE_BASH_DONE_{uuid.uuid4().hex}__" # unique per call314 self.process.stdin.write(f"{command}\necho {sentinel}\n")315 self.process.stdin.flush()316317 output = []318 for line in self.process.stdout:319 if sentinel in line: # this command's output is complete320 break321 output.append(line)322 return "".join(output)323324 def restart(self):325 self.process.kill()326 self.process.wait()327 self.__init__()328329330 bash_session = BashSession()331 print(bash_session.execute_command("cd /tmp && pwd"))332 print(bash_session.execute_command("pwd")) # still /tmp: the session kept its state333 ```334335 ```typescript TypeScript336 import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";337 import { createInterface, type Interface } from "node:readline";338 import { randomUUID } from "node:crypto";339340 // A bash process that stays alive between commands so state persists.341 class BashSession {342 process!: ChildProcessWithoutNullStreams;343 private lines!: Interface;344345 constructor() {346 this.start();347 }348349 private start(): void {350 this.process = spawn("/bin/bash", {351 detached: true // own process group: a timeout can kill every child352 });353 this.process.stdin.write("exec 2>&1\n"); // interleave errors with output, in order354 this.lines = createInterface({ input: this.process.stdout });355 }356357 // Run a command in the session and return its output.358 executeCommand(command: string): Promise<string> {359 const sentinel = `__CLAUDE_BASH_DONE_${randomUUID()}__`; // unique per call360 const output: string[] = [];361 const result = new Promise<string>((resolve) => {362 const onLine = (line: string): void => {363 if (line.includes(sentinel)) {364 // this command's output is complete365 this.lines.off("line", onLine);366 resolve(output.join(""));367 } else {368 output.push(`${line}\n`);369 }370 };371 this.lines.on("line", onLine);372 });373 this.process.stdin.write(`${command}\necho ${sentinel}\n`);374 return result;375 }376377 restart(): void {378 this.process.kill("SIGKILL");379 this.lines.close();380 this.start();381 }382 }383384 const session = new BashSession();385 console.log(await session.executeCommand("cd /tmp && pwd"));386 console.log(await session.executeCommand("pwd")); // still /tmp: the session kept its state387 session.process.stdin.end(); // closing stdin ends the shell so the script can exit388 ```389390 ```csharp C#391 using System.Diagnostics;392 using System.Text;393394 var session = new BashSession();395 Console.Write(session.ExecuteCommand("cd /tmp && pwd"));396 Console.Write(session.ExecuteCommand("pwd")); // still /tmp: the session kept its state397398 // A bash process that stays alive between commands so state persists.399 class BashSession400 {401 public Process Process { get; private set; }402403 public BashSession()404 {405 Process = Start();406 }407408 static Process Start()409 {410 var process = Process.Start(new ProcessStartInfo("/bin/bash")411 {412 RedirectStandardInput = true,413 RedirectStandardOutput = true414 })!;415 process.StandardInput.Write("exec 2>&1\n"); // interleave errors with output, in order416 process.StandardInput.Flush();417 return process;418 }419420 // Run a command in the session and return its output.421 public string ExecuteCommand(string command)422 {423 var sentinel = $"__CLAUDE_BASH_DONE_{Guid.NewGuid():N}__"; // unique per call424 Process.StandardInput.Write($"{command}\necho {sentinel}\n");425 Process.StandardInput.Flush();426427 var output = new StringBuilder();428 while (Process.StandardOutput.ReadLine() is string line)429 {430 if (line.Contains(sentinel)) // this command's output is complete431 {432 break;433 }434 output.Append(line).Append('\n');435 }436 return output.ToString();437 }438439 public void Restart()440 {441 Process.Kill(entireProcessTree: true);442 Process.WaitForExit();443 Process = Start();444 }445 }446 ```447448 ```go Go449 import (450 "bufio"451 "crypto/rand"452 "encoding/hex"453 "fmt"454 "io"455 "log"456 "os/exec"457 "strings"458 "syscall"459 )460461 // BashSession is a bash process that stays alive between commands so state persists.462 type BashSession struct {463 cmd *exec.Cmd464 stdin io.WriteCloser465 output *bufio.Reader466 }467468 func NewBashSession() (*BashSession, error) {469 cmd := exec.Command("/bin/bash")470 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // own process group: a timeout can kill every child471 stdin, err := cmd.StdinPipe()472 if err != nil {473 return nil, err474 }475 stdout, err := cmd.StdoutPipe()476 if err != nil {477 return nil, err478 }479 cmd.Stderr = cmd.Stdout // interleave errors with output, in order480 if err := cmd.Start(); err != nil {481 return nil, err482 }483 return &BashSession{cmd: cmd, stdin: stdin, output: bufio.NewReader(stdout)}, nil484 }485486 // ExecuteCommand runs a command in the session and returns its output.487 func (s *BashSession) ExecuteCommand(command string) string {488 buf := make([]byte, 16)489 rand.Read(buf)490 sentinel := fmt.Sprintf("__CLAUDE_BASH_DONE_%s__", hex.EncodeToString(buf)) // unique per call491 fmt.Fprintf(s.stdin, "%s\necho %s\n", command, sentinel)492493 var output strings.Builder494 for {495 line, err := s.output.ReadString('\n')496 if err != nil || strings.Contains(line, sentinel) { // this command's output is complete497 break498 }499 output.WriteString(line)500 }501 return output.String()502 }503504 // Restart kills the shell and starts a fresh session in its place.505 func (s *BashSession) Restart() error {506 s.cmd.Process.Kill()507 s.cmd.Wait()508 fresh, err := NewBashSession()509 if err != nil {510 return err511 }512 *s = *fresh513 return nil514 }515516 func main() {517 session, err := NewBashSession()518 if err != nil {519 log.Fatal(err)520 }521 fmt.Print(session.ExecuteCommand("cd /tmp && pwd"))522 fmt.Print(session.ExecuteCommand("pwd")) // still /tmp: the session kept its state523 }524 ```525526 ```java Java527 import java.io.BufferedReader;528 import java.io.BufferedWriter;529 import java.io.IOException;530 import java.io.InputStreamReader;531 import java.io.OutputStreamWriter;532 import java.util.UUID;533534 // A bash process that stays alive between commands so state persists.535 class BashSession {536 Process process;537 BufferedWriter stdin;538 BufferedReader output;539540 BashSession() throws IOException {541 start();542 }543544 void start() throws IOException {545 ProcessBuilder builder = new ProcessBuilder("/bin/bash");546 builder.redirectErrorStream(true); // interleave errors with output, in order547 process = builder.start();548 stdin = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));549 output = new BufferedReader(new InputStreamReader(process.getInputStream()));550 }551552 // Run a command in the session and return its output.553 String executeCommand(String command) throws IOException {554 String sentinel = "__CLAUDE_BASH_DONE_" + UUID.randomUUID() + "__"; // unique per call555 stdin.write(command + "\necho " + sentinel + "\n");556 stdin.flush();557558 StringBuilder result = new StringBuilder();559 String line;560 while ((line = output.readLine()) != null) {561 if (line.contains(sentinel)) { // this command's output is complete562 break;563 }564 result.append(line).append("\n");565 }566 return result.toString();567 }568569 void restart() throws IOException, InterruptedException {570 process.destroyForcibly();571 process.waitFor();572 start();573 }574 }575576 void main() throws Exception {577 BashSession session = new BashSession();578 IO.println(session.executeCommand("cd /tmp && pwd"));579 IO.println(session.executeCommand("pwd")); // still /tmp: the session kept its state580 }581 ```582583 ```php PHP584 // A bash process that stays alive between commands so state persists.585 class BashSession586 {587 public $process;588 public $stdin;589 public $output;590591 public function __construct()592 {593 $this->start();594 }595596 private function start(): void597 {598 // setsid gives the shell its own process group: a timeout can kill every child599 $this->process = proc_open(600 ['setsid', '/bin/bash'],601 [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['redirect', 1]], // interleave errors with output602 $pipes603 );604 $this->stdin = $pipes[0];605 $this->output = $pipes[1];606 }607608 // Run a command in the session and return its output.609 public function executeCommand(string $command): string610 {611 $sentinel = '__CLAUDE_BASH_DONE_' . bin2hex(random_bytes(16)) . '__'; // unique per call612 fwrite($this->stdin, "{$command}\necho {$sentinel}\n");613 fflush($this->stdin);614615 $output = '';616 while (($line = fgets($this->output)) !== false) {617 if (str_contains($line, $sentinel)) { // this command's output is complete618 break;619 }620 $output .= $line;621 }622 return $output;623 }624625 public function restart(): void626 {627 proc_terminate($this->process, 9);628 proc_close($this->process);629 $this->start();630 }631 }632633 $session = new BashSession();634 echo $session->executeCommand("cd /tmp && pwd");635 echo $session->executeCommand("pwd"); // still /tmp: the session kept its state636 ```637638 ```ruby Ruby639 require "open3"640 require "securerandom"641642 # A bash process that stays alive between commands so state persists.643 class BashSession644 attr_reader :output, :wait_thread645646 def initialize647 start648 end649650 # Run a command in the session and return its output.651 def execute_command(command)652 sentinel = "__CLAUDE_BASH_DONE_#{SecureRandom.hex(16)}__" # unique per call653 @stdin.write("#{command}\necho #{sentinel}\n")654 @stdin.flush655656 output = +""657 @output.each_line do |line|658 break if line.include?(sentinel) # this command's output is complete659660 output << line661 end662 output663 end664665 def restart666 Process.kill("KILL", @wait_thread.pid)667 @wait_thread.join668 start669 end670671 private672673 def start674 # popen2e interleaves errors with output, in order; pgroup gives the shell its675 # own process group so a timeout can kill every child676 @stdin, @output, @wait_thread = Open3.popen2e("/bin/bash", pgroup: true)677 end678 end679680 session = BashSession.new681 puts session.execute_command("cd /tmp && pwd")682 puts session.execute_command("pwd") # still /tmp: the session kept its state683 ```684 </CodeGroup>685686 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.687 </Step>688689 <Step title="Process Claude's tool calls">690 Extract and run commands from Claude's responses:691692 <CodeGroup exclude="shell">693 ```python Python694 tool_results = []695 for content in response.content:696 if content.type == "tool_use" and content.name == "bash":697 if content.input.get("restart"):698 bash_session.restart()699 result = "Bash session restarted"700 else:701 command = content.input.get("command")702 result = bash_session.execute_command(command)703704 # One tool_result per tool_use block, all returned in the next user message705 tool_results.append(706 {"type": "tool_result", "tool_use_id": content.id, "content": result}707 )708 ```709710 ```typescript TypeScript711 const toolResults: { type: string; tool_use_id: string; content: string }[] = [];712 for (const block of response.content) {713 if (block.type === "tool_use" && block.name === "bash") {714 let result: string;715 if (block.input.restart) {716 bashSession.restart();717 result = "Bash session restarted";718 } else {719 result = await bashSession.executeCommand(block.input.command ?? "");720 }721722 // One tool_result per tool_use block, all returned in the next user message723 toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result });724 }725 }726 ```727728 ```csharp C#729 var toolResults = new List<ToolResultBlockParam>();730 foreach (var block in response.Content)731 {732 if (block.TryPickToolUse(out var toolUse) && toolUse.Name == "bash")733 {734 string result;735 if (toolUse.Input.TryGetValue("restart", out var restart) && restart.GetBoolean())736 {737 bashSession.Restart();738 result = "Bash session restarted";739 }740 else741 {742 var command = toolUse.Input["command"].GetString() ?? "";743 result = bashSession.ExecuteCommand(command);744 }745746 // One tool_result per tool_use block, all returned in the next user message747 toolResults.Add(new ToolResultBlockParam { ToolUseID = toolUse.ID, Content = result });748 }749 }750 ```751752 ```go Go753 var toolResults []anthropic.ContentBlockParamUnion754 for _, block := range response.Content {755 if block.Type == "tool_use" && block.Name == "bash" {756 var input struct {757 Command string `json:"command"`758 Restart bool `json:"restart"`759 }760 if err := json.Unmarshal(block.Input, &input); err != nil {761 log.Fatal(err)762 }763764 var result string765 if input.Restart {766 bashSession.Restart()767 result = "Bash session restarted"768 } else {769 result = bashSession.ExecuteCommand(input.Command)770 }771772 // One tool_result per tool_use block, all returned in the next user message773 toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false))774 }775 }776 ```777778 ```java Java779 List<Map<String, Object>> toolResults = new ArrayList<>();780 for (ContentBlock block : response.content()) {781 if (block.type().equals("tool_use") && block.name().equals("bash")) {782 String result;783 if (Boolean.TRUE.equals(block.input().get("restart"))) {784 bashSession.restart();785 result = "Bash session restarted";786 } else {787 String command = (String) block.input().get("command");788 result = bashSession.executeCommand(command);789 }790791 // One tool_result per tool_use block, all returned in the next user message792 toolResults.add(Map.of("type", "tool_result", "tool_use_id", block.id(), "content", result));793 }794 }795 ```796797 ```php PHP798 $toolResults = [];799 foreach ($response->content as $block) {800 if ($block->type === 'tool_use' && $block->name === 'bash') {801 if (!empty($block->input['restart'])) {802 $bashSession->restart();803 $result = 'Bash session restarted';804 } else {805 $result = $bashSession->executeCommand($block->input['command']);806 }807808 // One tool_result per tool_use block, all returned in the next user message809 $toolResults[] = ['type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $result];810 }811 }812 ```813814 ```ruby Ruby815 tool_results = []816 response.content.each do |block|817 next unless block.type == :tool_use && block.name == "bash"818819 result =820 if block.input[:restart]821 bash_session.restart822 "Bash session restarted"823 else824 bash_session.execute_command(block.input[:command])825 end826827 # One tool_result per tool_use block, all returned in the next user message828 tool_results << {type: "tool_result", tool_use_id: block.id, content: result}829 end830 ```831 </CodeGroup>832 </Step>833834 <Step title="Return the result to Claude">835 Send the `tool_result` back in a `user` message that continues the same conversation. Claude either requests another command in the same session or finishes its answer:836837 <CodeGroup>838 ```bash cURL839 curl https://api.anthropic.com/v1/messages \840 -H "content-type: application/json" \841 -H "x-api-key: $ANTHROPIC_API_KEY" \842 -H "anthropic-version: 2023-06-01" \843 -d '{844 "model": "claude-opus-5",845 "max_tokens": 1024,846 "tools": [847 {848 "type": "bash_20250124",849 "name": "bash"850 }851 ],852 "messages": [853 {854 "role": "user",855 "content": "List all Python files in the current directory."856 },857 {858 "role": "assistant",859 "content": [860 {861 "type": "tool_use",862 "id": "toolu_01A09q90qw90lq917835lq9",863 "name": "bash",864 "input": {865 "command": "ls *.py"866 }867 }868 ]869 },870 {871 "role": "user",872 "content": [873 {874 "type": "tool_result",875 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",876 "content": "analysis.py\nprocess_data.py\n"877 }878 ]879 }880 ]881 }'882 ```883884 ```bash CLI885 ant messages create <<'YAML'886 model: claude-opus-5887 max_tokens: 1024888 tools:889 - type: bash_20250124890 name: bash891 messages:892 - role: user893 content: List all Python files in the current directory.894 - role: assistant895 content:896 - type: tool_use897 id: toolu_01A09q90qw90lq917835lq9898 name: bash899 input:900 command: ls *.py901 - role: user902 content:903 - type: tool_result904 tool_use_id: toolu_01A09q90qw90lq917835lq9905 content: |906 analysis.py907 process_data.py908 YAML909 ```910911 ```python Python912 client = anthropic.Anthropic()913914 response = client.messages.create(915 model="claude-opus-5",916 max_tokens=1024,917 tools=[{"type": "bash_20250124", "name": "bash"}],918 messages=[919 {"role": "user", "content": "List all Python files in the current directory."},920 {921 "role": "assistant",922 "content": [923 {924 "type": "tool_use",925 "id": "toolu_01A09q90qw90lq917835lq9",926 "name": "bash",927 "input": {"command": "ls *.py"},928 }929 ],930 },931 {932 "role": "user",933 "content": [934 {935 "type": "tool_result",936 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",937 "content": "analysis.py\nprocess_data.py\n",938 }939 ],940 },941 ],942 )943944 print(response.content)945 ```946947 ```typescript TypeScript948 const client = new Anthropic();949950 const response = await client.messages.create({951 model: "claude-opus-5",952 max_tokens: 1024,953 tools: [{ type: "bash_20250124", name: "bash" }],954 messages: [955 {956 role: "user",957 content: "List all Python files in the current directory."958 },959 {960 role: "assistant",961 content: [962 {963 type: "tool_use",964 id: "toolu_01A09q90qw90lq917835lq9",965 name: "bash",966 input: { command: "ls *.py" }967 }968 ]969 },970 {971 role: "user",972 content: [973 {974 type: "tool_result",975 tool_use_id: "toolu_01A09q90qw90lq917835lq9",976 content: "analysis.py\nprocess_data.py\n"977 }978 ]979 }980 ]981 });982983 console.log(response.content);984 ```985986 ```csharp C#987 var client = new AnthropicClient();988989 var response = await client.Messages.Create(990 new()991 {992 Model = Model.ClaudeOpus5,993 MaxTokens = 1024,994 Tools = [new ToolBash20250124()],995 Messages =996 [997 new()998 {999 Role = Role.User,1000 Content = "List all Python files in the current directory.",1001 },1002 new()1003 {1004 Role = Role.Assistant,1005 Content = new MessageParamContent(new List<ContentBlockParam>1006 {1007 new ContentBlockParam(new ToolUseBlockParam()1008 {1009 ID = "toolu_01A09q90qw90lq917835lq9",1010 Name = "bash",1011 Input = new Dictionary<string, JsonElement>1012 {1013 ["command"] = JsonSerializer.SerializeToElement("ls *.py"),1014 },1015 }),1016 }),1017 },1018 new()1019 {1020 Role = Role.User,1021 Content = new MessageParamContent(new List<ContentBlockParam>1022 {1023 new ContentBlockParam(new ToolResultBlockParam()1024 {1025 ToolUseID = "toolu_01A09q90qw90lq917835lq9",1026 Content = "analysis.py\nprocess_data.py\n",1027 }),1028 }),1029 },1030 ],1031 }1032 );10331034 Console.WriteLine(response);1035 ```10361037 ```go Go1038 client := anthropic.NewClient()10391040 response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{1041 Model: anthropic.ModelClaudeOpus5,1042 MaxTokens: 1024,1043 Tools: []anthropic.ToolUnionParam{1044 {OfBashTool20250124: &anthropic.ToolBash20250124Param{}},1045 },1046 Messages: []anthropic.MessageParam{1047 anthropic.NewUserMessage(anthropic.NewTextBlock("List all Python files in the current directory.")),1048 anthropic.NewAssistantMessage(1049 anthropic.NewToolUseBlock(1050 "toolu_01A09q90qw90lq917835lq9",1051 map[string]any{"command": "ls *.py"},1052 "bash",1053 ),1054 ),1055 anthropic.NewUserMessage(1056 anthropic.NewToolResultBlock(1057 "toolu_01A09q90qw90lq917835lq9",1058 "analysis.py\nprocess_data.py\n",1059 false,1060 ),1061 ),1062 },1063 })1064 if err != nil {1065 log.Fatal(err)1066 }1067 fmt.Println(response.Content)1068 ```10691070 ```java Java1071 import com.anthropic.core.JsonValue;1072 import com.anthropic.models.messages.ContentBlockParam;1073 // ...1074 import com.anthropic.models.messages.ToolBash20250124;1075 import com.anthropic.models.messages.ToolResultBlockParam;1076 import com.anthropic.models.messages.ToolUseBlockParam;1077 // ...1078 void main() {1079 AnthropicClient client = AnthropicOkHttpClient.fromEnv();10801081 MessageCreateParams params = MessageCreateParams.builder()1082 .model(Model.CLAUDE_OPUS_5)1083 .maxTokens(1024)1084 .addTool(ToolBash20250124.builder().build())1085 .addUserMessage("List all Python files in the current directory.")1086 .addAssistantMessageOfBlockParams(1087 List.of(1088 ContentBlockParam.ofToolUse(1089 ToolUseBlockParam.builder()1090 .id("toolu_01A09q90qw90lq917835lq9")1091 .name("bash")1092 .input(1093 ToolUseBlockParam.Input.builder()1094 .putAdditionalProperty("command", JsonValue.from("ls *.py"))1095 .build()1096 )1097 .build()1098 )1099 )1100 )1101 .addUserMessageOfBlockParams(1102 List.of(1103 ContentBlockParam.ofToolResult(1104 ToolResultBlockParam.builder()1105 .toolUseId("toolu_01A09q90qw90lq917835lq9")1106 .content("analysis.py\nprocess_data.py\n")1107 .build()1108 )1109 )1110 )1111 .build();11121113 Message response = client.messages().create(params);1114 IO.println(response.content());1115 }1116 ```11171118 ```php PHP1119 use Anthropic\Messages\ToolBash20250124;11201121 $client = new Client();11221123 $response = $client->messages->create(1124 model: 'claude-opus-5',1125 maxTokens: 1024,1126 tools: [new ToolBash20250124()],1127 messages: [1128 ['role' => 'user', 'content' => 'List all Python files in the current directory.'],1129 [1130 'role' => 'assistant',1131 'content' => [1132 [1133 'type' => 'tool_use',1134 'id' => 'toolu_01A09q90qw90lq917835lq9',1135 'name' => 'bash',1136 'input' => ['command' => 'ls *.py'],1137 ],1138 ],1139 ],1140 [1141 'role' => 'user',1142 'content' => [1143 [1144 'type' => 'tool_result',1145 'tool_use_id' => 'toolu_01A09q90qw90lq917835lq9',1146 'content' => "analysis.py\nprocess_data.py\n",1147 ],1148 ],1149 ],1150 ],1151 );11521153 print_r($response->content);1154 ```11551156 ```ruby Ruby1157 client = Anthropic::Client.new11581159 response = client.messages.create(1160 model: "claude-opus-5",1161 max_tokens: 1024,1162 tools: [{type: "bash_20250124", name: "bash"}],1163 messages: [1164 {role: "user", content: "List all Python files in the current directory."},1165 {1166 role: "assistant",1167 content: [1168 {1169 type: "tool_use",1170 id: "toolu_01A09q90qw90lq917835lq9",1171 name: "bash",1172 input: {command: "ls *.py"}1173 }1174 ]1175 },1176 {1177 role: "user",1178 content: [1179 {1180 type: "tool_result",1181 tool_use_id: "toolu_01A09q90qw90lq917835lq9",1182 content: "analysis.py\nprocess_data.py\n"1183 }1184 ]1185 }1186 ]1187 )11881189 puts response.content1190 ```1191 </CodeGroup>11921193 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).1194 </Step>11951196 <Step title="Implement safety measures">1197 Add validation and restrictions. Use an allowlist rather than a blocklist: a blocklist misses any command it didn't anticipate. The example also rejects shell operators that appear as separate words:11981199 <CodeGroup exclude="shell">1200 ```python Python1201 import shlex12021203 ALLOWED_COMMANDS = {"ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail"}1204 SHELL_OPERATORS = {"&&", "||", "|", ";", "&", ">", "<", ">>"}120512061207 def validate_command(command):1208 # Allow only commands from an explicit allowlist1209 try:1210 tokens = shlex.split(command)1211 except ValueError:1212 return False, "Could not parse command"12131214 if not tokens:1215 return False, "Empty command"12161217 executable = tokens[0]1218 if executable not in ALLOWED_COMMANDS:1219 return False, f"Command '{executable}' is not in the allowlist"12201221 # Reject shell operators written as separate words1222 for token in tokens[1:]:1223 if token in SHELL_OPERATORS or token.startswith(("$", "`")):1224 return False, f"Shell operator '{token}' is not allowed"12251226 return True, None1227 ```12281229 ```typescript TypeScript1230 const ALLOWED_COMMANDS = new Set([1231 "ls",1232 "cat",1233 "echo",1234 "pwd",1235 "grep",1236 "find",1237 "wc",1238 "head",1239 "tail"1240 ]);1241 const SHELL_OPERATORS = new Set(["&&", "||", "|", ";", "&", ">", "<", ">>"]);12421243 function validateCommand(command: string): { ok: boolean; reason?: string } {1244 // Split on whitespace: enough for a tripwire check1245 const tokens = command.split(/\s+/).filter((token) => token.length > 0);1246 if (tokens.length === 0) {1247 return { ok: false, reason: "Empty command" };1248 }12491250 // Allow only commands from an explicit allowlist1251 const executable = tokens[0];1252 if (!ALLOWED_COMMANDS.has(executable)) {1253 return { ok: false, reason: `Command '${executable}' is not in the allowlist` };1254 }12551256 // Reject shell operators written as separate words1257 for (const token of tokens.slice(1)) {1258 const bare = token.replace(/^["']+/, ""); // a quoted token can still smuggle an expansion1259 if (SHELL_OPERATORS.has(token) || bare.startsWith("$") || bare.startsWith("`")) {1260 return { ok: false, reason: `Shell operator '${token}' is not allowed` };1261 }1262 }12631264 return { ok: true };1265 }1266 ```12671268 ```csharp C#1269 var allowedCommands = new HashSet<string>1270 {1271 "ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail"1272 };1273 var shellOperators = new HashSet<string> { "&&", "||", "|", ";", "&", ">", "<", ">>" };12741275 (bool Ok, string? Reason) ValidateCommand(string command)1276 {1277 // Split on whitespace: enough for a tripwire check1278 var tokens = command.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);1279 if (tokens.Length == 0)1280 {1281 return (false, "Empty command");1282 }12831284 // Allow only commands from an explicit allowlist1285 var executable = tokens[0];1286 if (!allowedCommands.Contains(executable))1287 {1288 return (false, $"Command '{executable}' is not in the allowlist");1289 }12901291 // Reject shell operators written as separate words1292 foreach (var token in tokens.Skip(1))1293 {1294 var bare = token.TrimStart('"', '\''); // a quoted token can still smuggle an expansion1295 if (shellOperators.Contains(token) || bare.StartsWith('$') || bare.StartsWith('`'))1296 {1297 return (false, $"Shell operator '{token}' is not allowed");1298 }1299 }13001301 return (true, null);1302 }1303 ```13041305 ```go Go1306 var allowedCommands = map[string]bool{1307 "ls": true, "cat": true, "echo": true, "pwd": true, "grep": true,1308 "find": true, "wc": true, "head": true, "tail": true,1309 }13101311 var shellOperators = map[string]bool{1312 "&&": true, "||": true, "|": true, ";": true, "&": true,1313 ">": true, "<": true, ">>": true,1314 }13151316 func validateCommand(command string) (bool, string) {1317 // Split on whitespace: enough for a tripwire check1318 tokens := strings.Fields(command)1319 if len(tokens) == 0 {1320 return false, "Empty command"1321 }13221323 // Allow only commands from an explicit allowlist1324 executable := tokens[0]1325 if !allowedCommands[executable] {1326 return false, fmt.Sprintf("Command %q is not in the allowlist", executable)1327 }13281329 // Reject shell operators written as separate words1330 for _, token := range tokens[1:] {1331 bare := strings.TrimLeft(token, `"'`) // a quoted token can still smuggle an expansion1332 if shellOperators[token] || strings.HasPrefix(bare, "$") || strings.HasPrefix(bare, "`") {1333 return false, fmt.Sprintf("Shell operator %q is not allowed", token)1334 }1335 }13361337 return true, ""1338 }1339 ```13401341 ```java Java1342 import java.util.List;1343 import java.util.Set;13441345 static final Set<String> ALLOWED_COMMANDS =1346 Set.of("ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail");1347 static final Set<String> SHELL_OPERATORS = Set.of("&&", "||", "|", ";", "&", ">", "<", ">>");13481349 record Validation(boolean ok, String reason) {}13501351 Validation validateCommand(String command) {1352 // Split on whitespace: enough for a tripwire check1353 List<String> tokens = List.of(command.trim().split("\\s+"));1354 if (tokens.size() == 1 && tokens.get(0).isEmpty()) {1355 return new Validation(false, "Empty command");1356 }13571358 // Allow only commands from an explicit allowlist1359 String executable = tokens.get(0);1360 if (!ALLOWED_COMMANDS.contains(executable)) {1361 return new Validation(false, "Command '" + executable + "' is not in the allowlist");1362 }13631364 // Reject shell operators written as separate words1365 for (String token : tokens.subList(1, tokens.size())) {1366 String bare = token.replaceFirst("^[\"']+", ""); // a quoted token can still smuggle an expansion1367 if (SHELL_OPERATORS.contains(token) || bare.startsWith("$") || bare.startsWith("`")) {1368 return new Validation(false, "Shell operator '" + token + "' is not allowed");1369 }1370 }13711372 return new Validation(true, null);1373 }1374 ```13751376 ```php PHP1377 const ALLOWED_COMMANDS = ['ls', 'cat', 'echo', 'pwd', 'grep', 'find', 'wc', 'head', 'tail'];1378 const SHELL_OPERATORS = ['&&', '||', '|', ';', '&', '>', '<', '>>'];13791380 function validateCommand(string $command): array1381 {1382 // Split on whitespace: enough for a tripwire check1383 $tokens = preg_split('/\\s+/', trim($command), -1, PREG_SPLIT_NO_EMPTY);1384 if ($tokens === false || $tokens === []) {1385 return [false, 'Empty command'];1386 }13871388 // Allow only commands from an explicit allowlist1389 $executable = $tokens[0];1390 if (!in_array($executable, ALLOWED_COMMANDS, true)) {1391 return [false, "Command '{$executable}' is not in the allowlist"];1392 }13931394 // Reject shell operators written as separate words1395 foreach (array_slice($tokens, 1) as $token) {1396 $bare = ltrim($token, '"\''); // a quoted token can still smuggle an expansion1397 if (in_array($token, SHELL_OPERATORS, true) || str_starts_with($bare, '$') || str_starts_with($bare, '`')) {1398 return [false, "Shell operator '{$token}' is not allowed"];1399 }1400 }14011402 return [true, null];1403 }1404 ```14051406 ```ruby Ruby1407 require "shellwords"14081409 ALLOWED_COMMANDS = %w[ls cat echo pwd grep find wc head tail].freeze1410 SHELL_OPERATORS = ["&&", "||", "|", ";", "&", ">", "<", ">>"].freeze14111412 def validate_command(command)1413 # Allow only commands from an explicit allowlist1414 begin1415 tokens = Shellwords.split(command)1416 rescue ArgumentError1417 return [false, "Could not parse command"]1418 end14191420 return [false, "Empty command"] if tokens.empty?14211422 executable = tokens[0]1423 unless ALLOWED_COMMANDS.include?(executable)1424 return [false, "Command '#{executable}' is not in the allowlist"]1425 end14261427 # Reject shell operators written as separate words1428 tokens[1..].each do |token|1429 if SHELL_OPERATORS.include?(token) || token.start_with?("$", "`")1430 return [false, "Shell operator '#{token}' is not allowed"]1431 end1432 end14331434 [true, nil]1435 end1436 ```1437 </CodeGroup>14381439 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)).1440 </Step>1441</Steps>14421443### Handle errors14441445When 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](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls#handling-errors-with-is-error).14461447<AccordionGroup>1448 <Accordion title="Command execution timeout">1449 If a command takes too long to execute:14501451 ```json1452 {1453 "role": "user",1454 "content": [1455 {1456 "type": "tool_result",1457 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",1458 "content": "Error: command did not finish within 30 seconds",1459 "is_error": true1460 }1461 ]1462 }1463 ```1464 </Accordion>14651466 <Accordion title="Command not found">1467 If a command doesn't exist:14681469 ```json1470 {1471 "role": "user",1472 "content": [1473 {1474 "type": "tool_result",1475 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",1476 "content": "bash: nonexistentcommand: command not found",1477 "is_error": true1478 }1479 ]1480 }1481 ```1482 </Accordion>14831484 <Accordion title="Permission denied">1485 If there are permission issues:14861487 ```json1488 {1489 "role": "user",1490 "content": [1491 {1492 "type": "tool_result",1493 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",1494 "content": "bash: /root/sensitive-file: Permission denied",1495 "is_error": true1496 }1497 ]1498 }1499 ```1500 </Accordion>1501</AccordionGroup>15021503### Follow implementation best practices15041505<AccordionGroup>1506 <Accordion title="Use command timeouts">1507 A command that never finishes, such as one that waits for input, blocks the session forever because its sentinel line never arrives. Give every command a deadline. When the deadline passes, stop the shell and everything the command started, then restart the session:15081509 <CodeGroup exclude="shell">1510 ```python Python1511 import concurrent.futures1512 import os1513 import signal151415151516 def execute_with_timeout(session, command, timeout=30):1517 """Run a command in the session, replacing the session if the command hangs."""1518 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:1519 future = pool.submit(session.execute_command, command)1520 try:1521 return future.result(timeout=timeout)1522 except concurrent.futures.TimeoutError:1523 # The group is the shell and every process the command started1524 os.killpg(session.process.pid, signal.SIGKILL)1525 session.restart()1526 return f"Error: command did not finish within {timeout} seconds"1527 ```15281529 ```typescript TypeScript1530 // Run a command in the session, replacing the session if the command hangs.1531 async function executeWithTimeout(1532 session: BashSession,1533 command: string,1534 timeoutMs = 300001535 ): Promise<string> {1536 let timer: NodeJS.Timeout | undefined;1537 const timedOut = new Promise<never>((_, reject) => {1538 timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);1539 });1540 try {1541 return await Promise.race([session.executeCommand(command), timedOut]);1542 } catch {1543 // The group is the shell and every process the command started1544 if (session.process.pid !== undefined) {1545 process.kill(-session.process.pid, "SIGKILL");1546 }1547 session.restart();1548 return `Error: command did not finish within ${timeoutMs / 1000} seconds`;1549 } finally {1550 clearTimeout(timer);1551 }1552 }1553 ```15541555 ```csharp C#1556 using System.Diagnostics;15571558 // Run a command in the session, replacing the session if the command hangs.1559 static string ExecuteWithTimeout(BashSession session, string command, int timeoutSeconds = 30)1560 {1561 var work = Task.Run(() => session.ExecuteCommand(command));1562 if (work.Wait(TimeSpan.FromSeconds(timeoutSeconds)))1563 {1564 return work.Result;1565 }15661567 // Stop the shell and every process it started, then start a fresh session1568 session.Process.Kill(entireProcessTree: true);1569 session.Restart();1570 return $"Error: command did not finish within {timeoutSeconds} seconds";1571 }1572 ```15731574 ```go Go1575 // executeWithTimeout runs a command, replacing the session if the command hangs.1576 func executeWithTimeout(session *BashSession, command string, timeoutSeconds int) string {1577 done := make(chan string, 1)1578 go func() { done <- session.ExecuteCommand(command) }()15791580 select {1581 case result := <-done:1582 return result1583 case <-time.After(time.Duration(timeoutSeconds) * time.Second):1584 // The group is the shell and every process the command started1585 syscall.Kill(-session.cmd.Process.Pid, syscall.SIGKILL)1586 session.Restart()1587 return fmt.Sprintf("Error: command did not finish within %d seconds", timeoutSeconds)1588 }1589 }1590 ```15911592 ```java Java1593 // Run a command in the session, replacing the session if the command hangs.1594 String executeWithTimeout(BashSession session, String command, int timeoutSeconds) throws Exception {1595 ExecutorService pool = Executors.newSingleThreadExecutor();1596 try {1597 Future<String> future = pool.submit(() -> session.executeCommand(command));1598 return future.get(timeoutSeconds, TimeUnit.SECONDS);1599 } catch (TimeoutException e) {1600 // Stop the shell and every process it started, then start a fresh session1601 session.process.descendants().forEach(ProcessHandle::destroyForcibly);1602 session.process.destroyForcibly();1603 session.restart();1604 return "Error: command did not finish within " + timeoutSeconds + " seconds";1605 } finally {1606 pool.shutdownNow();1607 }1608 }1609 ```16101611 ```php PHP1612 // Run a command but give up if it does not finish within the deadline. PHP blocks on1613 // pipe reads, so the deadline lives inside the read loop: stream_select() waits for1614 // readable output before each fgets() so the loop can check the deadline.1615 function executeWithTimeout(BashSession $session, string $command, int $timeout = 30): string1616 {1617 $sentinel = '__CLAUDE_BASH_DONE_' . bin2hex(random_bytes(16)) . '__'; // unique per call1618 fwrite($session->stdin, "{$command}\necho {$sentinel}\n");1619 fflush($session->stdin);16201621 $deadline = microtime(true) + $timeout;1622 $output = '';1623 while (microtime(true) < $deadline) {1624 $read = [$session->output];1625 $write = null;1626 $except = null;1627 if (stream_select($read, $write, $except, 1) === 0) {1628 continue; // no output yet; check the deadline again1629 }1630 $line = fgets($session->output);1631 if ($line === false || str_contains($line, $sentinel)) {1632 return $output; // this command's output is complete1633 }1634 $output .= $line;1635 }16361637 // The group is the shell and every process the command started1638 posix_kill(-proc_get_status($session->process)['pid'], 9); // 9 = SIGKILL1639 $session->restart();1640 return "Error: command did not finish within {$timeout} seconds";1641 }1642 ```16431644 ```ruby Ruby1645 require "timeout"16461647 # Run a command in the session, replacing the session if the command hangs.1648 def execute_with_timeout(session, command, timeout: 30)1649 Timeout.timeout(timeout) { session.execute_command(command) }1650 rescue Timeout::Error1651 # The group is the shell and every process the command started1652 Process.kill("KILL", -session.wait_thread.pid)1653 session.restart1654 "Error: command did not finish within #{timeout} seconds"1655 end1656 ```1657 </CodeGroup>16581659 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.1660 </Accordion>16611662 <Accordion title="Maintain session state">1663 Keep the bash session persistent to maintain environment variables and working directory:16641665 <CodeGroup exclude="shell">1666 ```python Python1667 # Commands run in the same session maintain state1668 commands = [1669 "cd /tmp",1670 "echo 'Hello' > test.txt",1671 "cat test.txt", # The session is still in /tmp1672 ]1673 ```16741675 ```typescript TypeScript1676 // Commands run in the same session maintain state1677 const commands = [1678 "cd /tmp",1679 "echo 'Hello' > test.txt",1680 "cat test.txt" // The session is still in /tmp1681 ];1682 ```16831684 ```csharp C#1685 // Commands run in the same session maintain state1686 string[] commands =1687 [1688 "cd /tmp",1689 "echo 'Hello' > test.txt",1690 "cat test.txt", // The session is still in /tmp1691 ];1692 ```16931694 ```go Go1695 // Commands run in the same session maintain state1696 commands := []string{1697 "cd /tmp",1698 "echo 'Hello' > test.txt",1699 "cat test.txt", // The session is still in /tmp1700 }1701 ```17021703 ```java Java1704 // Commands run in the same session maintain state1705 List<String> commands = List.of(1706 "cd /tmp",1707 "echo 'Hello' > test.txt",1708 "cat test.txt" // The session is still in /tmp1709 );1710 ```17111712 ```php PHP1713 // Commands run in the same session maintain state1714 $commands = [1715 'cd /tmp',1716 "echo 'Hello' > test.txt",1717 'cat test.txt', // The session is still in /tmp1718 ];1719 ```17201721 ```ruby Ruby1722 # Commands run in the same session maintain state1723 commands = [1724 "cd /tmp",1725 "echo 'Hello' > test.txt",1726 "cat test.txt" # The session is still in /tmp1727 ]1728 ```1729 </CodeGroup>1730 </Accordion>17311732 <Accordion title="Handle large outputs">1733 Truncate large outputs to prevent token limit issues:17341735 <CodeGroup exclude="shell">1736 ```python Python1737 def truncate_output(output, max_lines=100):1738 lines = output.split("\n")1739 if len(lines) > max_lines:1740 truncated = "\n".join(lines[:max_lines])1741 return f"{truncated}\n\n... Output truncated ({len(lines)} total lines) ..."1742 return output1743 ```17441745 ```typescript TypeScript1746 function truncateOutput(output: string, maxLines = 100): string {1747 const lines = output.split("\n");1748 if (lines.length > maxLines) {1749 const truncated = lines.slice(0, maxLines).join("\n");1750 return `${truncated}\n\n... Output truncated (${lines.length} total lines) ...`;1751 }1752 return output;1753 }1754 ```17551756 ```csharp C#1757 string TruncateOutput(string output, int maxLines = 100)1758 {1759 var lines = output.Split('\n');1760 if (lines.Length > maxLines)1761 {1762 var truncated = string.Join("\n", lines.Take(maxLines));1763 return $"{truncated}\n\n... Output truncated ({lines.Length} total lines) ...";1764 }1765 return output;1766 }1767 ```17681769 ```go Go1770 func truncateOutput(output string, maxLines int) string {1771 lines := strings.Split(output, "\n")1772 if len(lines) > maxLines {1773 truncated := strings.Join(lines[:maxLines], "\n")1774 return fmt.Sprintf("%s\n\n... Output truncated (%d total lines) ...", truncated, len(lines))1775 }1776 return output1777 }1778 ```17791780 ```java Java1781 String truncateOutput(String output, int maxLines) {1782 String[] lines = output.split("\n", -1);1783 if (lines.length > maxLines) {1784 String truncated = String.join("\n", Arrays.copyOf(lines, maxLines));1785 return truncated + "\n\n... Output truncated (" + lines.length + " total lines) ...";1786 }1787 return output;1788 }1789 ```17901791 ```php PHP1792 function truncateOutput(string $output, int $maxLines = 100): string1793 {1794 $lines = explode("\n", $output);1795 if (count($lines) > $maxLines) {1796 $truncated = implode("\n", array_slice($lines, 0, $maxLines));1797 return "{$truncated}\n\n... Output truncated (" . count($lines) . ' total lines) ...';1798 }1799 return $output;1800 }1801 ```18021803 ```ruby Ruby1804 def truncate_output(output, max_lines: 100)1805 lines = output.split("\n", -1)1806 return output unless lines.length > max_lines18071808 truncated = lines.first(max_lines).join("\n")1809 "#{truncated}\n\n... Output truncated (#{lines.length} total lines) ..."1810 end1811 ```1812 </CodeGroup>1813 </Accordion>18141815 <Accordion title="Log all commands">1816 Keep an audit trail. Route every command through one wrapper that records the command before it runs and the output after it finishes. A command that hangs or breaks the session still leaves a record:18171818 <CodeGroup exclude="shell">1819 ```python Python1820 import logging18211822 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")182318241825 def execute_and_log(session, command):1826 """Run a command in the session and keep an audit record of it."""1827 logging.info("command=%r", command)1828 output = session.execute_command(command)1829 logging.info("output=%r", output[:200]) # first 200 characters1830 return output1831 ```18321833 ```typescript TypeScript1834 // Run a command in the session and keep an audit record of it.1835 async function executeAndLog(session: BashSession, command: string): Promise<string> {1836 console.error(`command=${JSON.stringify(command)}`);1837 const output = await session.executeCommand(command);1838 console.error(`output=${JSON.stringify(output.slice(0, 200))}`); // first 200 characters1839 return output;1840 }1841 ```18421843 ```csharp C#1844 // Run a command in the session and keep an audit record of it.1845 static string ExecuteAndLog(BashSession session, string command)1846 {1847 Console.Error.WriteLine($"command={command}");1848 var output = session.ExecuteCommand(command);1849 Console.Error.WriteLine($"output={output[..Math.Min(output.Length, 200)]}"); // first 200 characters1850 return output;1851 }1852 ```18531854 ```go Go1855 // executeAndLog runs a command in the session and keeps an audit record of it.1856 func executeAndLog(session *BashSession, command string) string {1857 log.Printf("command=%q", command)1858 output := session.ExecuteCommand(command)1859 log.Printf("output=%q", output[:min(len(output), 200)]) // first 200 characters1860 return output1861 }1862 ```18631864 ```java Java1865 static final Logger AUDIT = Logger.getLogger("bash-audit");18661867 // Run a command in the session and keep an audit record of it.1868 String executeAndLog(BashSession session, String command) throws IOException {1869 AUDIT.info("command=" + command);1870 String output = session.executeCommand(command);1871 AUDIT.info("output=" + output.substring(0, Math.min(output.length(), 200))); // first 200 characters1872 return output;1873 }1874 ```18751876 ```php PHP1877 // Run a command in the session and keep an audit record of it.1878 function executeAndLog(BashSession $session, string $command): string1879 {1880 error_log("command={$command}");1881 $output = $session->executeCommand($command);1882 error_log('output=' . substr($output, 0, 200)); // first 200 characters1883 return $output;1884 }1885 ```18861887 ```ruby Ruby1888 require "logger"18891890 AUDIT = Logger.new($stderr)18911892 # Run a command in the session and keep an audit record of it.1893 def execute_and_log(session, command)1894 AUDIT.info("command=#{command.inspect}")1895 output = session.execute_command(command)1896 AUDIT.info("output=#{output[0, 200].inspect}") # first 200 characters1897 output1898 end1899 ```1900 </CodeGroup>19011902 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`.1903 </Accordion>1904</AccordionGroup>19051906## Security19071908<Warning>1909 Your application runs whatever command Claude requests. Run the session in an isolated environment, such as a container or a virtual machine, as the least-privileged user that can do the work. Treat every command as untrusted input.1910</Warning>19111912Beyond isolation, add these controls:19131914* Validate commands before running them, with an allowlist rather than a blocklist. See [Implement the bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#implement-the-bash-tool).1915* Set resource limits on the shell process (CPU, memory, and disk), for example with `ulimit`.1916* Log every command and its output so you can audit what ran.1917* Redact credentials and other secrets from output before returning it to Claude.19181919## Pricing19201921The bash tool definition adds the following input tokens to your request. This is in addition to the per-model [tool use system prompt](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#pricing) that applies whenever any tool is present.19221923| Model | Additional input tokens |1924| --------------------------------------------------- | ----------------------- |1925| Claude Opus 5, Claude Opus 4.8, and Claude Opus 4.7 | 325 tokens |1926| Claude Opus 4.6, Claude Sonnet 4.6, and earlier | 244 tokens |19271928Additional tokens are consumed by:19291930* Command outputs (stdout/stderr)1931* Error messages1932* Large file contents19331934See [tool use pricing](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#pricing) for complete pricing details.19351936## Common patterns19371938### Development workflows19391940* Running tests: `pytest && coverage report`1941* Building projects: `npm install && npm run build`1942* Git operations: `git status && git add . && git commit -m "message"`19431944For guidance on using git as a checkpoint-and-recovery mechanism in long-running agent workflows, see [state management best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#state-management-best-practices).19451946### File operations19471948* Processing data: `wc -l *.csv && ls -lh *.csv`1949* Searching files: `find . -name "*.py" | xargs grep "pattern"`1950* Creating backups: `tar -czf backup.tar.gz ./data`19511952### System tasks19531954* Checking resources: `df -h && free -m`1955* Process management: `ps aux | grep python`1956* Environment setup: `export PATH=$PATH:/new/path && echo $PATH`19571958## Limitations19591960* **No interactive commands:** The session can't run `vim`, `less`, password prompts, or any command that waits for input on stdin.1961* **No GUI applications:** The session is command-line only.1962* **Session scope:** Bash session state is client-side. Your application is responsible for maintaining the shell session between turns.1963* **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.1964* **No streaming:** Output reaches Claude only when your application returns the `tool_result` in the next request.19651966## Combining with other tools19671968The bash tool pairs well with the [Text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool): Claude edits a file with one tool and requests the command that runs it with the other.19691970<Note>1971 If you're also using the [Code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool), Claude has access to two separate execution environments: your local bash session and Anthropic's sandboxed container. State is not shared between them. See [Using code execution with other execution tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#using-code-execution-with-other-execution-tools) for guidance on prompting Claude to distinguish between environments.1972</Note>19731974## Next steps19751976<CardGroup cols={2}>1977 <Card title="Text editor tool" icon="file" href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool">1978 View and modify text files to debug, fix, and improve code.1979 </Card>19801981 <Card title="Tool use with Claude" icon="tool" href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview">1982 Connect Claude to external tools and APIs. See where tools execute, when Claude calls them, and which tool fits your task.1983 </Card>1984</CardGroup>1985