Bash tool
On this page
The bash tool is a client tool: Claude doesn't run commands itself. When you include the tool in a request, Claude replies with a tool_use block that names the command to run. Your application runs that command in a bash session it owns and returns the output in a tool_result block.
Your application keeps one bash process alive across tool calls, so state persists between commands. The working directory, environment variables, and any files a command creates are still there for the next command.
The current version of the tool is bash_20250124. For model support, beta headers, and the earlier version, see Tool versions. For all Anthropic-provided tools, see the Tool reference.
Use cases
- Development workflows: Run build commands, tests, and development tools
- System automation: Execute scripts, manage files, automate tasks
- Data processing: Process files, run analysis scripts, manage datasets
- Environment setup: Install packages, configure environments
Quick start
ant messages create \
--model claude-opus-5 \
--max-tokens 1024 \
--tool '{type: bash_20250124, name: bash}' \
--message '{role: user, content: List all Python files in the current directory.}'
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=[{"type": "bash_20250124", "name": "bash"}],
messages=[
{"role": "user", "content": "List all Python files in the current directory."}
],
)
print(response)
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 1024,
tools: [{ type: "bash_20250124", name: "bash" }],
messages: [
{
role: "user",
content: "List all Python files in the current directory."
}
]
});
console.log(response);
var client = new AnthropicClient();
var response = await client.Messages.Create(
new()
{
Model = Model.ClaudeOpus5,
MaxTokens = 1024,
Tools = [new ToolBash20250124()],
Messages =
[
new()
{
Role = Role.User,
Content = "List all Python files in the current directory.",
},
],
}
);
Console.WriteLine(response);
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5,
MaxTokens: 1024,
Tools: []anthropic.ToolUnionParam{
{OfBashTool20250124: &anthropic.ToolBash20250124Param{}},
},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("List all Python files in the current directory.")),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response)
import com.anthropic.models.messages.ToolBash20250124;
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5)
.maxTokens(1024)
.addTool(ToolBash20250124.builder().build())
.addUserMessage("List all Python files in the current directory.")
.build()
);
IO.println(response);
}
use Anthropic\Messages\ToolBash20250124;
$client = new Client();
$response = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 1024,
tools: [new ToolBash20250124()],
messages: [
['role' => 'user', 'content' => 'List all Python files in the current directory.'],
],
);
echo $response;
client = Anthropic::Client.new
response = client.messages.create(
model: "claude-opus-5",
max_tokens: 1024,
tools: [{type: "bash_20250124", name: "bash"}],
messages: [
{role: "user", content: "List all Python files in the current directory."}
]
)
puts response
Claude responds with stop_reason: "tool_use" and a tool_use block that contains the command for your application to run:
{
"id": "msg_01XAbCDeFgHiJkLmNoPQrStU",
"model": "claude-opus-5",
"stop_reason": "tool_use",
"role": "assistant",
"content": [
{
"type": "text",
"text": "I'll list all Python files in the current directory for you."
},
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "bash",
"input": {
"command": "ls *.py"
}
}
]
}
Run input.command in your bash session and send the output back as a tool_result. See Implement the bash tool for the round trip.
How it works
Each tool call is one round trip between Claude and your application:
- Claude returns a
tool_useblock containing thecommandto run. - Your application runs the command in its bash session.
- Your application returns the command's output, stdout and stderr together, to Claude in a
tool_resultblock. - Claude either requests another command in the same session or responds with text.
Claude can also return several tool_use blocks in one response. Run them in order in the same session and return all of the results in one user message. See Parallel tool use.
The API is stateless. Nothing about your shell session travels between requests, so your application decides when the session starts, how long it lives, and when to restart it. For the full request and response cycle, see Handle tool calls.
Parameters
A bash tool definition has two required fields, type and name, and the name must be bash. The tool is schema-less: you don't provide an input_schema, because the schema is built into Claude's model and can't be modified. The following table lists the input fields Claude sets when it calls the tool.
| Parameter | Required | Description |
|---|---|---|
command | Yes* | The bash command to run |
restart | No | Set to true to restart the bash session |
*Required unless using restart
To handle restart: true, kill the shell process, start a new one, and return a tool_result that confirms the restart. A restarted session starts clean: the working directory, environment variables, and any running processes are gone.
{
"command": "ls -la *.py"
}
Restart the session:
{
"restart": true
}
Tool versions
bash_20250124 is the current version of the tool, and it requires no beta header. Every model from Claude Sonnet 3.7 (retired) onward accepts it, including all current Claude models.
The original bash_20241022 version works only with the October 2024 Claude Sonnet 3.5 model (retired). Requests that use it need the anthropic-beta: computer-use-2024-10-22 header, and the SDKs expose it only in their beta namespaces. New integrations should use bash_20250124.
Example: Multistep automation
Claude can chain commands across tool calls to complete a multistep task:
User request:
"Install the requests library and create a simple Python script that
fetches a joke from an API, then run it."
Claude's tool uses:
1. Install package
{"command": "pip install requests"}
2. Create script
{"command": "cat > fetch_joke.py << 'EOF'\nimport requests\nresponse = requests.get('https://official-joke-api.appspot.com/random_joke')\njoke = response.json()\nprint(f\"Setup: {joke['setup']}\")\nprint(f\"Punchline: {joke['punchline']}\")\nEOF"}
3. Run script
{"command": "python fetch_joke.py"}
The session maintains state between commands, so files created in step 2 are available in step 3.
Implement the bash tool
Claude determines which command to run. Your application owns everything else: the shell process, the timeout, and the safety checks. The following steps show a minimal implementation.
<CodeGroup exclude="shell">
```python Python
import subprocess
import uuid
class BashSession:
"""A bash process that stays alive between commands so state persists."""
def __init__(self):
self.process = subprocess.Popen(
["/bin/bash"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # interleave errors with output, in order
start_new_session=True, # own process group: a timeout can kill every child
text=True,
)
def execute_command(self, command):
"""Run a command in the session and return its output."""
sentinel = f"__CLAUDE_BASH_DONE_{uuid.uuid4().hex}__" # unique per call
self.process.stdin.write(f"{command}\necho {sentinel}\n")
self.process.stdin.flush()
output = []
for line in self.process.stdout:
if sentinel in line: # this command's output is complete
break
output.append(line)
return "".join(output)
def restart(self):
self.process.kill()
self.process.wait()
self.__init__()
bash_session = BashSession()
print(bash_session.execute_command("cd /tmp && pwd"))
print(bash_session.execute_command("pwd")) # still /tmp: the session kept its state
```
```typescript TypeScript
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { createInterface, type Interface } from "node:readline";
import { randomUUID } from "node:crypto";
// A bash process that stays alive between commands so state persists.
class BashSession {
process!: ChildProcessWithoutNullStreams;
private lines!: Interface;
constructor() {
this.start();
}
private start(): void {
this.process = spawn("/bin/bash", {
detached: true // own process group: a timeout can kill every child
});
this.process.stdin.write("exec 2>&1\n"); // interleave errors with output, in order
this.lines = createInterface({ input: this.process.stdout });
}
// Run a command in the session and return its output.
executeCommand(command: string): Promise<string> {
const sentinel = `__CLAUDE_BASH_DONE_${randomUUID()}__`; // unique per call
const output: string[] = [];
const result = new Promise<string>((resolve) => {
const onLine = (line: string): void => {
if (line.includes(sentinel)) {
// this command's output is complete
this.lines.off("line", onLine);
resolve(output.join(""));
} else {
output.push(`${line}\n`);
}
};
this.lines.on("line", onLine);
});
this.process.stdin.write(`${command}\necho ${sentinel}\n`);
return result;
}
restart(): void {
this.process.kill("SIGKILL");
this.lines.close();
this.start();
}
}
const session = new BashSession();
console.log(await session.executeCommand("cd /tmp && pwd"));
console.log(await session.executeCommand("pwd")); // still /tmp: the session kept its state
session.process.stdin.end(); // closing stdin ends the shell so the script can exit
```
```csharp C#
using System.Diagnostics;
using System.Text;
var session = new BashSession();
Console.Write(session.ExecuteCommand("cd /tmp && pwd"));
Console.Write(session.ExecuteCommand("pwd")); // still /tmp: the session kept its state
// A bash process that stays alive between commands so state persists.
class BashSession
{
public Process Process { get; private set; }
public BashSession()
{
Process = Start();
}
static Process Start()
{
var process = Process.Start(new ProcessStartInfo("/bin/bash")
{
RedirectStandardInput = true,
RedirectStandardOutput = true
})!;
process.StandardInput.Write("exec 2>&1\n"); // interleave errors with output, in order
process.StandardInput.Flush();
return process;
}
// Run a command in the session and return its output.
public string ExecuteCommand(string command)
{
var sentinel = $"__CLAUDE_BASH_DONE_{Guid.NewGuid():N}__"; // unique per call
Process.StandardInput.Write($"{command}\necho {sentinel}\n");
Process.StandardInput.Flush();
var output = new StringBuilder();
while (Process.StandardOutput.ReadLine() is string line)
{
if (line.Contains(sentinel)) // this command's output is complete
{
break;
}
output.Append(line).Append('\n');
}
return output.ToString();
}
public void Restart()
{
Process.Kill(entireProcessTree: true);
Process.WaitForExit();
Process = Start();
}
}
```
```go Go
import (
"bufio"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"log"
"os/exec"
"strings"
"syscall"
)
// BashSession is a bash process that stays alive between commands so state persists.
type BashSession struct {
cmd *exec.Cmd
stdin io.WriteCloser
output *bufio.Reader
}
func NewBashSession() (*BashSession, error) {
cmd := exec.Command("/bin/bash")
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // own process group: a timeout can kill every child
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
cmd.Stderr = cmd.Stdout // interleave errors with output, in order
if err := cmd.Start(); err != nil {
return nil, err
}
return &BashSession{cmd: cmd, stdin: stdin, output: bufio.NewReader(stdout)}, nil
}
// ExecuteCommand runs a command in the session and returns its output.
func (s *BashSession) ExecuteCommand(command string) string {
buf := make([]byte, 16)
rand.Read(buf)
sentinel := fmt.Sprintf("__CLAUDE_BASH_DONE_%s__", hex.EncodeToString(buf)) // unique per call
fmt.Fprintf(s.stdin, "%s\necho %s\n", command, sentinel)
var output strings.Builder
for {
line, err := s.output.ReadString('\n')
if err != nil || strings.Contains(line, sentinel) { // this command's output is complete
break
}
output.WriteString(line)
}
return output.String()
}
// Restart kills the shell and starts a fresh session in its place.
func (s *BashSession) Restart() error {
s.cmd.Process.Kill()
s.cmd.Wait()
fresh, err := NewBashSession()
if err != nil {
return err
}
*s = *fresh
return nil
}
func main() {
session, err := NewBashSession()
if err != nil {
log.Fatal(err)
}
fmt.Print(session.ExecuteCommand("cd /tmp && pwd"))
fmt.Print(session.ExecuteCommand("pwd")) // still /tmp: the session kept its state
}
```
```java Java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.UUID;
// A bash process that stays alive between commands so state persists.
class BashSession {
Process process;
BufferedWriter stdin;
BufferedReader output;
BashSession() throws IOException {
start();
}
void start() throws IOException {
ProcessBuilder builder = new ProcessBuilder("/bin/bash");
builder.redirectErrorStream(true); // interleave errors with output, in order
process = builder.start();
stdin = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
output = new BufferedReader(new InputStreamReader(process.getInputStream()));
}
// Run a command in the session and return its output.
String executeCommand(String command) throws IOException {
String sentinel = "__CLAUDE_BASH_DONE_" + UUID.randomUUID() + "__"; // unique per call
stdin.write(command + "\necho " + sentinel + "\n");
stdin.flush();
StringBuilder result = new StringBuilder();
String line;
while ((line = output.readLine()) != null) {
if (line.contains(sentinel)) { // this command's output is complete
break;
}
result.append(line).append("\n");
}
return result.toString();
}
void restart() throws IOException, InterruptedException {
process.destroyForcibly();
process.waitFor();
start();
}
}
void main() throws Exception {
BashSession session = new BashSession();
IO.println(session.executeCommand("cd /tmp && pwd"));
IO.println(session.executeCommand("pwd")); // still /tmp: the session kept its state
}
```
```php PHP
// A bash process that stays alive between commands so state persists.
class BashSession
{
public $process;
public $stdin;
public $output;
public function __construct()
{
$this->start();
}
private function start(): void
{
// setsid gives the shell its own process group: a timeout can kill every child
$this->process = proc_open(
['setsid', '/bin/bash'],
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['redirect', 1]], // interleave errors with output
$pipes
);
$this->stdin = $pipes[0];
$this->output = $pipes[1];
}
// Run a command in the session and return its output.
public function executeCommand(string $command): string
{
$sentinel = '__CLAUDE_BASH_DONE_' . bin2hex(random_bytes(16)) . '__'; // unique per call
fwrite($this->stdin, "{$command}\necho {$sentinel}\n");
fflush($this->stdin);
$output = '';
while (($line = fgets($this->output)) !== false) {
if (str_contains($line, $sentinel)) { // this command's output is complete
break;
}
$output .= $line;
}
return $output;
}
public function restart(): void
{
proc_terminate($this->process, 9);
proc_close($this->process);
$this->start();
}
}
$session = new BashSession();
echo $session->executeCommand("cd /tmp && pwd");
echo $session->executeCommand("pwd"); // still /tmp: the session kept its state
```
```ruby Ruby
require "open3"
require "securerandom"
# A bash process that stays alive between commands so state persists.
class BashSession
attr_reader :output, :wait_thread
def initialize
start
end
# Run a command in the session and return its output.
def execute_command(command)
sentinel = "__CLAUDE_BASH_DONE_#{SecureRandom.hex(16)}__" # unique per call
@stdin.write("#{command}\necho #{sentinel}\n")
@stdin.flush
output = +""
@output.each_line do |line|
break if line.include?(sentinel) # this command's output is complete
output << line
end
output
end
def restart
Process.kill("KILL", @wait_thread.pid)
@wait_thread.join
start
end
private
def start
# popen2e interleaves errors with output, in order; pgroup gives the shell its
# own process group so a timeout can kill every child
@stdin, @output, @wait_thread = Open3.popen2e("/bin/bash", pgroup: true)
end
end
session = BashSession.new
puts session.execute_command("cd /tmp && pwd")
puts session.execute_command("pwd") # still /tmp: the session kept its state
```
</CodeGroup>
The session interleaves stderr with stdout, so error messages land where they happened. The example leaves out what a complete implementation also needs: a timeout that kills the shell and every process it started when a command hangs, then restarts the session. The [Use command timeouts](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#follow-implementation-best-practices) best practice shows one way to add it.
<CodeGroup exclude="shell">
```python Python
tool_results = []
for content in response.content:
if content.type == "tool_use" and content.name == "bash":
if content.input.get("restart"):
bash_session.restart()
result = "Bash session restarted"
else:
command = content.input.get("command")
result = bash_session.execute_command(command)
# One tool_result per tool_use block, all returned in the next user message
tool_results.append(
{"type": "tool_result", "tool_use_id": content.id, "content": result}
)
```
```typescript TypeScript
const toolResults: { type: string; tool_use_id: string; content: string }[] = [];
for (const block of response.content) {
if (block.type === "tool_use" && block.name === "bash") {
let result: string;
if (block.input.restart) {
bashSession.restart();
result = "Bash session restarted";
} else {
result = await bashSession.executeCommand(block.input.command ?? "");
}
// One tool_result per tool_use block, all returned in the next user message
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result });
}
}
```
```csharp C#
var toolResults = new List<ToolResultBlockParam>();
foreach (var block in response.Content)
{
if (block.TryPickToolUse(out var toolUse) && toolUse.Name == "bash")
{
string result;
if (toolUse.Input.TryGetValue("restart", out var restart) && restart.GetBoolean())
{
bashSession.Restart();
result = "Bash session restarted";
}
else
{
var command = toolUse.Input["command"].GetString() ?? "";
result = bashSession.ExecuteCommand(command);
}
// One tool_result per tool_use block, all returned in the next user message
toolResults.Add(new ToolResultBlockParam { ToolUseID = toolUse.ID, Content = result });
}
}
```
```go Go
var toolResults []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
if block.Type == "tool_use" && block.Name == "bash" {
var input struct {
Command string `json:"command"`
Restart bool `json:"restart"`
}
if err := json.Unmarshal(block.Input, &input); err != nil {
log.Fatal(err)
}
var result string
if input.Restart {
bashSession.Restart()
result = "Bash session restarted"
} else {
result = bashSession.ExecuteCommand(input.Command)
}
// One tool_result per tool_use block, all returned in the next user message
toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false))
}
}
```
```java Java
List<Map<String, Object>> toolResults = new ArrayList<>();
for (ContentBlock block : response.content()) {
if (block.type().equals("tool_use") && block.name().equals("bash")) {
String result;
if (Boolean.TRUE.equals(block.input().get("restart"))) {
bashSession.restart();
result = "Bash session restarted";
} else {
String command = (String) block.input().get("command");
result = bashSession.executeCommand(command);
}
// One tool_result per tool_use block, all returned in the next user message
toolResults.add(Map.of("type", "tool_result", "tool_use_id", block.id(), "content", result));
}
}
```
```php PHP
$toolResults = [];
foreach ($response->content as $block) {
if ($block->type === 'tool_use' && $block->name === 'bash') {
if (!empty($block->input['restart'])) {
$bashSession->restart();
$result = 'Bash session restarted';
} else {
$result = $bashSession->executeCommand($block->input['command']);
}
// One tool_result per tool_use block, all returned in the next user message
$toolResults[] = ['type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $result];
}
}
```
```ruby Ruby
tool_results = []
response.content.each do |block|
next unless block.type == :tool_use && block.name == "bash"
result =
if block.input[:restart]
bash_session.restart
"Bash session restarted"
else
bash_session.execute_command(block.input[:command])
end
# One tool_result per tool_use block, all returned in the next user message
tool_results << {type: "tool_result", tool_use_id: block.id, content: result}
end
```
</CodeGroup>
<CodeGroup>
```bash cURL
curl https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"tools": [
{
"type": "bash_20250124",
"name": "bash"
}
],
"messages": [
{
"role": "user",
"content": "List all Python files in the current directory."
},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "bash",
"input": {
"command": "ls *.py"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "analysis.py\nprocess_data.py\n"
}
]
}
]
}'
```
```bash CLI
ant messages create <<'YAML'
model: claude-opus-5
max_tokens: 1024
tools:
- type: bash_20250124
name: bash
messages:
- role: user
content: List all Python files in the current directory.
- role: assistant
content:
- type: tool_use
id: toolu_01A09q90qw90lq917835lq9
name: bash
input:
command: ls *.py
- role: user
content:
- type: tool_result
tool_use_id: toolu_01A09q90qw90lq917835lq9
content: |
analysis.py
process_data.py
YAML
```
```python Python
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=[{"type": "bash_20250124", "name": "bash"}],
messages=[
{"role": "user", "content": "List all Python files in the current directory."},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "bash",
"input": {"command": "ls *.py"},
}
],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "analysis.py\nprocess_data.py\n",
}
],
},
],
)
print(response.content)
```
```typescript TypeScript
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 1024,
tools: [{ type: "bash_20250124", name: "bash" }],
messages: [
{
role: "user",
content: "List all Python files in the current directory."
},
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_01A09q90qw90lq917835lq9",
name: "bash",
input: { command: "ls *.py" }
}
]
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "toolu_01A09q90qw90lq917835lq9",
content: "analysis.py\nprocess_data.py\n"
}
]
}
]
});
console.log(response.content);
```
```csharp C#
var client = new AnthropicClient();
var response = await client.Messages.Create(
new()
{
Model = Model.ClaudeOpus5,
MaxTokens = 1024,
Tools = [new ToolBash20250124()],
Messages =
[
new()
{
Role = Role.User,
Content = "List all Python files in the current directory.",
},
new()
{
Role = Role.Assistant,
Content = new MessageParamContent(new List<ContentBlockParam>
{
new ContentBlockParam(new ToolUseBlockParam()
{
ID = "toolu_01A09q90qw90lq917835lq9",
Name = "bash",
Input = new Dictionary<string, JsonElement>
{
["command"] = JsonSerializer.SerializeToElement("ls *.py"),
},
}),
}),
},
new()
{
Role = Role.User,
Content = new MessageParamContent(new List<ContentBlockParam>
{
new ContentBlockParam(new ToolResultBlockParam()
{
ToolUseID = "toolu_01A09q90qw90lq917835lq9",
Content = "analysis.py\nprocess_data.py\n",
}),
}),
},
],
}
);
Console.WriteLine(response);
```
```go Go
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5,
MaxTokens: 1024,
Tools: []anthropic.ToolUnionParam{
{OfBashTool20250124: &anthropic.ToolBash20250124Param{}},
},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("List all Python files in the current directory.")),
anthropic.NewAssistantMessage(
anthropic.NewToolUseBlock(
"toolu_01A09q90qw90lq917835lq9",
map[string]any{"command": "ls *.py"},
"bash",
),
),
anthropic.NewUserMessage(
anthropic.NewToolResultBlock(
"toolu_01A09q90qw90lq917835lq9",
"analysis.py\nprocess_data.py\n",
false,
),
),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Content)
```
```java Java
import com.anthropic.core.JsonValue;
import com.anthropic.models.messages.ContentBlockParam;
// ...
import com.anthropic.models.messages.ToolBash20250124;
import com.anthropic.models.messages.ToolResultBlockParam;
import com.anthropic.models.messages.ToolUseBlockParam;
// ...
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5)
.maxTokens(1024)
.addTool(ToolBash20250124.builder().build())
.addUserMessage("List all Python files in the current directory.")
.addAssistantMessageOfBlockParams(
List.of(
ContentBlockParam.ofToolUse(
ToolUseBlockParam.builder()
.id("toolu_01A09q90qw90lq917835lq9")
.name("bash")
.input(
ToolUseBlockParam.Input.builder()
.putAdditionalProperty("command", JsonValue.from("ls *.py"))
.build()
)
.build()
)
)
)
.addUserMessageOfBlockParams(
List.of(
ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder()
.toolUseId("toolu_01A09q90qw90lq917835lq9")
.content("analysis.py\nprocess_data.py\n")
.build()
)
)
)
.build();
Message response = client.messages().create(params);
IO.println(response.content());
}
```
```php PHP
use Anthropic\Messages\ToolBash20250124;
$client = new Client();
$response = $client->messages->create(
model: 'claude-opus-5',
maxTokens: 1024,
tools: [new ToolBash20250124()],
messages: [
['role' => 'user', 'content' => 'List all Python files in the current directory.'],
[
'role' => 'assistant',
'content' => [
[
'type' => 'tool_use',
'id' => 'toolu_01A09q90qw90lq917835lq9',
'name' => 'bash',
'input' => ['command' => 'ls *.py'],
],
],
],
[
'role' => 'user',
'content' => [
[
'type' => 'tool_result',
'tool_use_id' => 'toolu_01A09q90qw90lq917835lq9',
'content' => "analysis.py\nprocess_data.py\n",
],
],
],
],
);
print_r($response->content);
```
```ruby Ruby
client = Anthropic::Client.new
response = client.messages.create(
model: "claude-opus-5",
max_tokens: 1024,
tools: [{type: "bash_20250124", name: "bash"}],
messages: [
{role: "user", content: "List all Python files in the current directory."},
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_01A09q90qw90lq917835lq9",
name: "bash",
input: {command: "ls *.py"}
}
]
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "toolu_01A09q90qw90lq917835lq9",
content: "analysis.py\nprocess_data.py\n"
}
]
}
]
)
puts response.content
```
</CodeGroup>
Repeat the run-and-return cycle while `stop_reason` is `tool_use`. For the full loop, see [Handling results from client tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls#handling-results-from-client-tools).
<CodeGroup exclude="shell">
```python Python
import shlex
ALLOWED_COMMANDS = {"ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail"}
SHELL_OPERATORS = {"&&", "||", "|", ";", "&", ">", "<", ">>"}
def validate_command(command):
# Allow only commands from an explicit allowlist
try:
tokens = shlex.split(command)
except ValueError:
return False, "Could not parse command"
if not tokens:
return False, "Empty command"
executable = tokens[0]
if executable not in ALLOWED_COMMANDS:
return False, f"Command '{executable}' is not in the allowlist"
# Reject shell operators written as separate words
for token in tokens[1:]:
if token in SHELL_OPERATORS or token.startswith(("$", "`")):
return False, f"Shell operator '{token}' is not allowed"
return True, None
```
```typescript TypeScript
const ALLOWED_COMMANDS = new Set([
"ls",
"cat",
"echo",
"pwd",
"grep",
"find",
"wc",
"head",
"tail"
]);
const SHELL_OPERATORS = new Set(["&&", "||", "|", ";", "&", ">", "<", ">>"]);
function validateCommand(command: string): { ok: boolean; reason?: string } {
// Split on whitespace: enough for a tripwire check
const tokens = command.split(/\s+/).filter((token) => token.length > 0);
if (tokens.length === 0) {
return { ok: false, reason: "Empty command" };
}
// Allow only commands from an explicit allowlist
const executable = tokens[0];
if (!ALLOWED_COMMANDS.has(executable)) {
return { ok: false, reason: `Command '${executable}' is not in the allowlist` };
}
// Reject shell operators written as separate words
for (const token of tokens.slice(1)) {
const bare = token.replace(/^["']+/, ""); // a quoted token can still smuggle an expansion
if (SHELL_OPERATORS.has(token) || bare.startsWith("$") || bare.startsWith("`")) {
return { ok: false, reason: `Shell operator '${token}' is not allowed` };
}
}
return { ok: true };
}
```
```csharp C#
var allowedCommands = new HashSet<string>
{
"ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail"
};
var shellOperators = new HashSet<string> { "&&", "||", "|", ";", "&", ">", "<", ">>" };
(bool Ok, string? Reason) ValidateCommand(string command)
{
// Split on whitespace: enough for a tripwire check
var tokens = command.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length == 0)
{
return (false, "Empty command");
}
// Allow only commands from an explicit allowlist
var executable = tokens[0];
if (!allowedCommands.Contains(executable))
{
return (false, $"Command '{executable}' is not in the allowlist");
}
// Reject shell operators written as separate words
foreach (var token in tokens.Skip(1))
{
var bare = token.TrimStart('"', '\''); // a quoted token can still smuggle an expansion
if (shellOperators.Contains(token) || bare.StartsWith('$') || bare.StartsWith('`'))
{
return (false, $"Shell operator '{token}' is not allowed");
}
}
return (true, null);
}
```
```go Go
var allowedCommands = map[string]bool{
"ls": true, "cat": true, "echo": true, "pwd": true, "grep": true,
"find": true, "wc": true, "head": true, "tail": true,
}
var shellOperators = map[string]bool{
"&&": true, "||": true, "|": true, ";": true, "&": true,
">": true, "<": true, ">>": true,
}
func validateCommand(command string) (bool, string) {
// Split on whitespace: enough for a tripwire check
tokens := strings.Fields(command)
if len(tokens) == 0 {
return false, "Empty command"
}
// Allow only commands from an explicit allowlist
executable := tokens[0]
if !allowedCommands[executable] {
return false, fmt.Sprintf("Command %q is not in the allowlist", executable)
}
// Reject shell operators written as separate words
for _, token := range tokens[1:] {
bare := strings.TrimLeft(token, `"'`) // a quoted token can still smuggle an expansion
if shellOperators[token] || strings.HasPrefix(bare, "$") || strings.HasPrefix(bare, "`") {
return false, fmt.Sprintf("Shell operator %q is not allowed", token)
}
}
return true, ""
}
```
```java Java
import java.util.List;
import java.util.Set;
static final Set<String> ALLOWED_COMMANDS =
Set.of("ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail");
static final Set<String> SHELL_OPERATORS = Set.of("&&", "||", "|", ";", "&", ">", "<", ">>");
record Validation(boolean ok, String reason) {}
Validation validateCommand(String command) {
// Split on whitespace: enough for a tripwire check
List<String> tokens = List.of(command.trim().split("\\s+"));
if (tokens.size() == 1 && tokens.get(0).isEmpty()) {
return new Validation(false, "Empty command");
}
// Allow only commands from an explicit allowlist
String executable = tokens.get(0);
if (!ALLOWED_COMMANDS.contains(executable)) {
return new Validation(false, "Command '" + executable + "' is not in the allowlist");
}
// Reject shell operators written as separate words
for (String token : tokens.subList(1, tokens.size())) {
String bare = token.replaceFirst("^[\"']+", ""); // a quoted token can still smuggle an expansion
if (SHELL_OPERATORS.contains(token) || bare.startsWith("$") || bare.startsWith("`")) {
return new Validation(false, "Shell operator '" + token + "' is not allowed");
}
}
return new Validation(true, null);
}
```
```php PHP
const ALLOWED_COMMANDS = ['ls', 'cat', 'echo', 'pwd', 'grep', 'find', 'wc', 'head', 'tail'];
const SHELL_OPERATORS = ['&&', '||', '|', ';', '&', '>', '<', '>>'];
function validateCommand(string $command): array
{
// Split on whitespace: enough for a tripwire check
$tokens = preg_split('/\\s+/', trim($command), -1, PREG_SPLIT_NO_EMPTY);
if ($tokens === false || $tokens === []) {
return [false, 'Empty command'];
}
// Allow only commands from an explicit allowlist
$executable = $tokens[0];
if (!in_array($executable, ALLOWED_COMMANDS, true)) {
return [false, "Command '{$executable}' is not in the allowlist"];
}
// Reject shell operators written as separate words
foreach (array_slice($tokens, 1) as $token) {
$bare = ltrim($token, '"\''); // a quoted token can still smuggle an expansion
if (in_array($token, SHELL_OPERATORS, true) || str_starts_with($bare, '$') || str_starts_with($bare, '`')) {
return [false, "Shell operator '{$token}' is not allowed"];
}
}
return [true, null];
}
```
```ruby Ruby
require "shellwords"
ALLOWED_COMMANDS = %w[ls cat echo pwd grep find wc head tail].freeze
SHELL_OPERATORS = ["&&", "||", "|", ";", "&", ">", "<", ">>"].freeze
def validate_command(command)
# Allow only commands from an explicit allowlist
begin
tokens = Shellwords.split(command)
rescue ArgumentError
return [false, "Could not parse command"]
end
return [false, "Empty command"] if tokens.empty?
executable = tokens[0]
unless ALLOWED_COMMANDS.include?(executable)
return [false, "Command '#{executable}' is not in the allowlist"]
end
# Reject shell operators written as separate words
tokens[1..].each do |token|
if SHELL_OPERATORS.include?(token) || token.start_with?("$", "`")
return [false, "Shell operator '#{token}' is not allowed"]
end
end
[true, nil]
end
```
</CodeGroup>
This check is a tripwire for obvious mistakes, not an enforcement boundary. It rejects the spaced chaining (`&&`), pipes, and redirection that the other examples on this page use. It does not catch an operator glued to a word, such as `cat data.txt|grep x`, because the tokenizer keeps `data.txt|grep` inside one token. Decide which commands and operators your application allows. The real control is isolation: run the whole session inside a container or a virtual machine (see [Security](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#security)).
Handle errors
When a command fails or the session breaks, tell Claude what happened. Return the message as the tool_result content and set is_error to true, which marks the tool call as failed. See Handling errors with is_error.
```json
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "Error: command did not finish within 30 seconds",
"is_error": true
}
]
}
```
```json
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "bash: nonexistentcommand: command not found",
"is_error": true
}
]
}
```
```json
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "bash: /root/sensitive-file: Permission denied",
"is_error": true
}
]
}
```
Follow implementation best practices
<CodeGroup exclude="shell">
```python Python
import concurrent.futures
import os
import signal
def execute_with_timeout(session, command, timeout=30):
"""Run a command in the session, replacing the session if the command hangs."""
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(session.execute_command, command)
try:
return future.result(timeout=timeout)
except concurrent.futures.TimeoutError:
# The group is the shell and every process the command started
os.killpg(session.process.pid, signal.SIGKILL)
session.restart()
return f"Error: command did not finish within {timeout} seconds"
```
```typescript TypeScript
// Run a command in the session, replacing the session if the command hangs.
async function executeWithTimeout(
session: BashSession,
command: string,
timeoutMs = 30000
): Promise<string> {
let timer: NodeJS.Timeout | undefined;
const timedOut = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
});
try {
return await Promise.race([session.executeCommand(command), timedOut]);
} catch {
// The group is the shell and every process the command started
if (session.process.pid !== undefined) {
process.kill(-session.process.pid, "SIGKILL");
}
session.restart();
return `Error: command did not finish within ${timeoutMs / 1000} seconds`;
} finally {
clearTimeout(timer);
}
}
```
```csharp C#
using System.Diagnostics;
// Run a command in the session, replacing the session if the command hangs.
static string ExecuteWithTimeout(BashSession session, string command, int timeoutSeconds = 30)
{
var work = Task.Run(() => session.ExecuteCommand(command));
if (work.Wait(TimeSpan.FromSeconds(timeoutSeconds)))
{
return work.Result;
}
// Stop the shell and every process it started, then start a fresh session
session.Process.Kill(entireProcessTree: true);
session.Restart();
return $"Error: command did not finish within {timeoutSeconds} seconds";
}
```
```go Go
// executeWithTimeout runs a command, replacing the session if the command hangs.
func executeWithTimeout(session *BashSession, command string, timeoutSeconds int) string {
done := make(chan string, 1)
go func() { done <- session.ExecuteCommand(command) }()
select {
case result := <-done:
return result
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
// The group is the shell and every process the command started
syscall.Kill(-session.cmd.Process.Pid, syscall.SIGKILL)
session.Restart()
return fmt.Sprintf("Error: command did not finish within %d seconds", timeoutSeconds)
}
}
```
```java Java
// Run a command in the session, replacing the session if the command hangs.
String executeWithTimeout(BashSession session, String command, int timeoutSeconds) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
try {
Future<String> future = pool.submit(() -> session.executeCommand(command));
return future.get(timeoutSeconds, TimeUnit.SECONDS);
} catch (TimeoutException e) {
// Stop the shell and every process it started, then start a fresh session
session.process.descendants().forEach(ProcessHandle::destroyForcibly);
session.process.destroyForcibly();
session.restart();
return "Error: command did not finish within " + timeoutSeconds + " seconds";
} finally {
pool.shutdownNow();
}
}
```
```php PHP
// Run a command but give up if it does not finish within the deadline. PHP blocks on
// pipe reads, so the deadline lives inside the read loop: stream_select() waits for
// readable output before each fgets() so the loop can check the deadline.
function executeWithTimeout(BashSession $session, string $command, int $timeout = 30): string
{
$sentinel = '__CLAUDE_BASH_DONE_' . bin2hex(random_bytes(16)) . '__'; // unique per call
fwrite($session->stdin, "{$command}\necho {$sentinel}\n");
fflush($session->stdin);
$deadline = microtime(true) + $timeout;
$output = '';
while (microtime(true) < $deadline) {
$read = [$session->output];
$write = null;
$except = null;
if (stream_select($read, $write, $except, 1) === 0) {
continue; // no output yet; check the deadline again
}
$line = fgets($session->output);
if ($line === false || str_contains($line, $sentinel)) {
return $output; // this command's output is complete
}
$output .= $line;
}
// The group is the shell and every process the command started
posix_kill(-proc_get_status($session->process)['pid'], 9); // 9 = SIGKILL
$session->restart();
return "Error: command did not finish within {$timeout} seconds";
}
```
```ruby Ruby
require "timeout"
# Run a command in the session, replacing the session if the command hangs.
def execute_with_timeout(session, command, timeout: 30)
Timeout.timeout(timeout) { session.execute_command(command) }
rescue Timeout::Error
# The group is the shell and every process the command started
Process.kill("KILL", -session.wait_thread.pid)
session.restart
"Error: command did not finish within #{timeout} seconds"
end
```
</CodeGroup>
The kill stops the hung command and everything it started. Return the message as an error `tool_result` (see [Handle errors](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#handle-errors)), which marks the tool call as failed.
<CodeGroup exclude="shell">
```python Python
# Commands run in the same session maintain state
commands = [
"cd /tmp",
"echo 'Hello' > test.txt",
"cat test.txt", # The session is still in /tmp
]
```
```typescript TypeScript
// Commands run in the same session maintain state
const commands = [
"cd /tmp",
"echo 'Hello' > test.txt",
"cat test.txt" // The session is still in /tmp
];
```
```csharp C#
// Commands run in the same session maintain state
string[] commands =
[
"cd /tmp",
"echo 'Hello' > test.txt",
"cat test.txt", // The session is still in /tmp
];
```
```go Go
// Commands run in the same session maintain state
commands := []string{
"cd /tmp",
"echo 'Hello' > test.txt",
"cat test.txt", // The session is still in /tmp
}
```
```java Java
// Commands run in the same session maintain state
List<String> commands = List.of(
"cd /tmp",
"echo 'Hello' > test.txt",
"cat test.txt" // The session is still in /tmp
);
```
```php PHP
// Commands run in the same session maintain state
$commands = [
'cd /tmp',
"echo 'Hello' > test.txt",
'cat test.txt', // The session is still in /tmp
];
```
```ruby Ruby
# Commands run in the same session maintain state
commands = [
"cd /tmp",
"echo 'Hello' > test.txt",
"cat test.txt" # The session is still in /tmp
]
```
</CodeGroup>
<CodeGroup exclude="shell">
```python Python
def truncate_output(output, max_lines=100):
lines = output.split("\n")
if len(lines) > max_lines:
truncated = "\n".join(lines[:max_lines])
return f"{truncated}\n\n... Output truncated ({len(lines)} total lines) ..."
return output
```
```typescript TypeScript
function truncateOutput(output: string, maxLines = 100): string {
const lines = output.split("\n");
if (lines.length > maxLines) {
const truncated = lines.slice(0, maxLines).join("\n");
return `${truncated}\n\n... Output truncated (${lines.length} total lines) ...`;
}
return output;
}
```
```csharp C#
string TruncateOutput(string output, int maxLines = 100)
{
var lines = output.Split('\n');
if (lines.Length > maxLines)
{
var truncated = string.Join("\n", lines.Take(maxLines));
return $"{truncated}\n\n... Output truncated ({lines.Length} total lines) ...";
}
return output;
}
```
```go Go
func truncateOutput(output string, maxLines int) string {
lines := strings.Split(output, "\n")
if len(lines) > maxLines {
truncated := strings.Join(lines[:maxLines], "\n")
return fmt.Sprintf("%s\n\n... Output truncated (%d total lines) ...", truncated, len(lines))
}
return output
}
```
```java Java
String truncateOutput(String output, int maxLines) {
String[] lines = output.split("\n", -1);
if (lines.length > maxLines) {
String truncated = String.join("\n", Arrays.copyOf(lines, maxLines));
return truncated + "\n\n... Output truncated (" + lines.length + " total lines) ...";
}
return output;
}
```
```php PHP
function truncateOutput(string $output, int $maxLines = 100): string
{
$lines = explode("\n", $output);
if (count($lines) > $maxLines) {
$truncated = implode("\n", array_slice($lines, 0, $maxLines));
return "{$truncated}\n\n... Output truncated (" . count($lines) . ' total lines) ...';
}
return $output;
}
```
```ruby Ruby
def truncate_output(output, max_lines: 100)
lines = output.split("\n", -1)
return output unless lines.length > max_lines
truncated = lines.first(max_lines).join("\n")
"#{truncated}\n\n... Output truncated (#{lines.length} total lines) ..."
end
```
</CodeGroup>
<CodeGroup exclude="shell">
```python Python
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
def execute_and_log(session, command):
"""Run a command in the session and keep an audit record of it."""
logging.info("command=%r", command)
output = session.execute_command(command)
logging.info("output=%r", output[:200]) # first 200 characters
return output
```
```typescript TypeScript
// Run a command in the session and keep an audit record of it.
async function executeAndLog(session: BashSession, command: string): Promise<string> {
console.error(`command=${JSON.stringify(command)}`);
const output = await session.executeCommand(command);
console.error(`output=${JSON.stringify(output.slice(0, 200))}`); // first 200 characters
return output;
}
```
```csharp C#
// Run a command in the session and keep an audit record of it.
static string ExecuteAndLog(BashSession session, string command)
{
Console.Error.WriteLine($"command={command}");
var output = session.ExecuteCommand(command);
Console.Error.WriteLine($"output={output[..Math.Min(output.Length, 200)]}"); // first 200 characters
return output;
}
```
```go Go
// executeAndLog runs a command in the session and keeps an audit record of it.
func executeAndLog(session *BashSession, command string) string {
log.Printf("command=%q", command)
output := session.ExecuteCommand(command)
log.Printf("output=%q", output[:min(len(output), 200)]) // first 200 characters
return output
}
```
```java Java
static final Logger AUDIT = Logger.getLogger("bash-audit");
// Run a command in the session and keep an audit record of it.
String executeAndLog(BashSession session, String command) throws IOException {
AUDIT.info("command=" + command);
String output = session.executeCommand(command);
AUDIT.info("output=" + output.substring(0, Math.min(output.length(), 200))); // first 200 characters
return output;
}
```
```php PHP
// Run a command in the session and keep an audit record of it.
function executeAndLog(BashSession $session, string $command): string
{
error_log("command={$command}");
$output = $session->executeCommand($command);
error_log('output=' . substr($output, 0, 200)); // first 200 characters
return $output;
}
```
```ruby Ruby
require "logger"
AUDIT = Logger.new($stderr)
# Run a command in the session and keep an audit record of it.
def execute_and_log(session, command)
AUDIT.info("command=#{command.inspect}")
output = session.execute_command(command)
AUDIT.info("output=#{output[0, 200].inspect}") # first 200 characters
output
end
```
</CodeGroup>
The records go to `stderr` by default; point them at a file or your logging pipeline to keep them. Include whatever ties the record to the request in your application, such as the end user and the `tool_use_id`.
Security
Beyond isolation, add these controls:
- Validate commands before running them, with an allowlist rather than a blocklist. See Implement the bash tool.
- Set resource limits on the shell process (CPU, memory, and disk), for example with
ulimit. - Log every command and its output so you can audit what ran.
- Redact credentials and other secrets from output before returning it to Claude.
Pricing
The bash tool definition adds the following input tokens to your request. This is in addition to the per-model tool use system prompt that applies whenever any tool is present.
| Model | Additional input tokens |
|---|---|
| Claude Opus 5, Claude Opus 4.8, and Claude Opus 4.7 | 325 tokens |
| Claude Opus 4.6, Claude Sonnet 4.6, and earlier | 244 tokens |
Additional tokens are consumed by:
- Command outputs (stdout/stderr)
- Error messages
- Large file contents
See tool use pricing for complete pricing details.
Common patterns
Development workflows
- Running tests:
pytest && coverage report - Building projects:
npm install && npm run build - Git operations:
git status && git add . && git commit -m "message"
For guidance on using git as a checkpoint-and-recovery mechanism in long-running agent workflows, see state management best practices.
File operations
- Processing data:
wc -l *.csv && ls -lh *.csv - Searching files:
find . -name "*.py" | xargs grep "pattern" - Creating backups:
tar -czf backup.tar.gz ./data
System tasks
- Checking resources:
df -h && free -m - Process management:
ps aux | grep python - Environment setup:
export PATH=$PATH:/new/path && echo $PATH
Limitations
- No interactive commands: The session can't run
vim,less, password prompts, or any command that waits for input on stdin. - No GUI applications: The session is command-line only.
- Session scope: Bash session state is client-side. Your application is responsible for maintaining the shell session between turns.
- Output limits: The API doesn't truncate tool results (an oversized request is rejected). Truncate large outputs in your application before returning them to Claude.
- No streaming: Output reaches Claude only when your application returns the
tool_resultin the next request.
Combining with other tools
The bash tool pairs well with the Text editor tool: Claude edits a file with one tool and requests the command that runs it with the other.