SPB Git

Repository Public

bash-tool.md 69.6 KB 1,985 lines markdown

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
Files main
Menu

Theme