Bash 도구
Bash 도구 (Bash tool)
bash 도구는 클라이언트 도구예요. Claude는 명령을 직접 실행하지 않아요. 요청에 도구를 포함하면 Claude가 실행할 명령을 명명하는 tool_use 블록으로 응답해요. 여러분의 애플리케이션은 자체 bash 세션에서 그 명령을 실행하고 출력을 tool_result 블록으로 반환해요.
출처: 문서
본문
참고 (Note) 이 기능에 제로 데이터 보존(ZDR)이 어떻게 적용되는지는 API 및 데이터 보존을 참고하세요.
bash 도구는 클라이언트 도구예요. Claude는 명령을 직접 실행하지 않아요. 요청에 도구를 포함하면 Claude가 실행할 명령을 명명하는 tool_use 블록으로 응답해요. 여러분의 애플리케이션은 자체 bash 세션에서 그 명령을 실행하고 출력을 tool_result 블록으로 반환해요.
여러분의 애플리케이션은 도구 호출 간에 하나의 bash 프로세스를 살려두므로 명령 사이에 상태가 지속돼요. 작업 디렉터리, 환경 변수, 명령이 만든 어떤 파일도 다음 명령을 위해 그대로 남아 있어요.
도구의 현재 버전은 bash_20250124이에요. 모델 지원, 베타 헤더, 이전 버전은 도구 버전을 참고하세요. 모든 Anthropic 제공 도구는 도구 레퍼런스를 참고하세요.
사용 사례 (Use cases)
- 개발 워크플로우: 빌드 명령, 테스트, 개발 도구 실행
- 시스템 자동화: 스크립트 실행, 파일 관리, 작업 자동화
- 데이터 처리: 파일 처리, 분석 스크립트 실행, 데이터셋 관리
- 환경 설정: 패키지 설치, 환경 구성
빠른 시작 (Quick start)
ant messages create \
--model claude-opus-5-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-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-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_5,
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_5,
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_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-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-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는 stop_reason: "tool_use"와 여러분의 애플리케이션이 실행할 명령이 담긴 tool_use 블록으로 응답해요:
{
"id": "msg_01XAbCDeFgHiJkLmNoPQrStU",
"model": "claude-opus-5-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"
}
}
]
}
input.command를 bash 세션에서 실행하고 출력을 tool_result로 다시 보내세요. 왕복은 Bash 도구 구현하기를 참고하세요.
작동 방식 (How it works)
각 도구 호출은 Claude와 여러분의 애플리케이션 사이의 왕복 한 번이에요:
- Claude는 실행할
command가 담긴tool_use블록을 반환해요. - 여러분의 애플리케이션이 bash 세션에서 명령을 실행해요.
- 여러분의 애플리케이션이 명령의 출력(stdout과 stderr를 함께)을
tool_result블록으로 Claude에 반환해요. - Claude는 같은 세션에서 다른 명령을 요청하거나 텍스트로 응답해요.
Claude는 한 응답에서 여러 tool_use 블록을 반환할 수도 있어요. 같은 세션에서 순서대로 실행하고 모든 결과를 하나의 user 메시지로 반환하세요. 병렬 도구 사용을 참고하세요.
API는 무상태(stateless)예요. 셸 세션에 관한 어떤 것도 요청 사이에 이동하지 않으므로, 세션이 언제 시작하고 얼마나 오래 살고 언제 재시작할지는 여러분의 애플리케이션이 결정해요. 전체 요청·응답 주기는 도구 호출 처리하기를 참고하세요.
파라미터 (Parameters)
bash 도구 정의에는 type과 name 두 필수 필드가 있고, name은 bash여야 해요. 도구는 스키마가 없어요. input_schema를 제공하지 않아요. 스키마가 Claude의 모델에 내장되어 수정할 수 없기 때문이에요. 다음 표는 Claude가 도구를 호출할 때 설정하는 입력 필드를 나열해요.
| 파라미터 | 필수 | 설명 |
|---|---|---|
command |
예* | 실행할 bash 명령 |
restart |
아니요 | bash 세션을 재시작하려면 true로 설정 |
*restart 사용을 제외하고 필수
restart: true를 처리하려면 셸 프로세스를 죽이고 새로 시작한 다음 재시작을 확인하는 tool_result를 반환하세요. 재시작된 세션은 깨끗하게 시작돼요. 작업 디렉터리, 환경 변수, 실행 중인 프로세스가 모두 사라져요.
사용 예시: 명령 실행:
{
"command": "ls -la *.py"
}
세션 재시작:
{
"restart": true
}
도구 버전 (Tool versions)
bash_20250124는 도구의 현재 버전이며 베타 헤더가 필요 없어요. Claude Sonnet 3.7(폐지) 이후 모든 모델이 받으며, 현재의 모든 Claude 모델을 포함해요.
원래 bash_20241022 버전은 2024년 10월 Claude Sonnet 3.5 모델(폐지)에서만 동작해요. 그것을 쓰는 요청은 anthropic-beta: computer-use-2024-10-22 헤더가 필요하고, SDK는 베타 네임스페이스에서만 노출해요. 새 통합은 bash_20250124를 사용하세요.
예시: 다단계 자동화 (Example: Multistep automation)
Claude는 도구 호출 간에 명령을 연결해서 다단계 작업을 완료할 수 있어요:
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"}
세션은 명령 사이에 상태를 유지하므로 2단계에서 만든 파일이 3단계에서 사용 가능해요.
Bash 도구 구현하기 (Implement the bash tool)
Claude가 실행할 명령을 결정해요. 여러분의 애플리케이션이 셸 프로세스, 타임아웃, 안전 확인을 포함한 모든 나머지를 소유해요. 다음 단계는 최소 구현을 보여줘요.
-
영구 bash 세션 만들기 — 하나의 장수(long-lived) bash 프로세스를 시작하고 모든 명령을 그 안에서 실행해요. 라이브 프로세스로의 파이프는 파일 끝을 보고하지 않으므로, 세션은 각 명령 후에 고유한 센티널 줄을 출력해 그 명령의 출력이 어디서 끝나는지 표시해요:
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 stateimport { 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 exitusing 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(); } }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 }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 }// 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 staterequire "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세션은 stderr를 stdout과 함께 인터리브해서 오류 메시지가 발생한 곳에 나타나게 해요. 예시는 완전한 구현에 필요한 것, 즉 명령이 멈출 때 셸과 그것이 시작한 모든 프로세스를 죽이고 세션을 재시작하는 타임아웃을 생략해요. 명령 타임아웃 사용하기 모범 사례가 추가하는 한 가지 방법을 보여줘요.
-
Claude의 도구 호출 처리하기 — Claude 응답에서 명령을 추출하고 실행해요:
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} )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 }); } }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 }); } }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)) } }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)); } }$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]; } }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 -
결과를 Claude에 반환하기 —
tool_result를 같은 대화를 계속하는user메시지로 보내요. Claude는 같은 세션에서 다른 명령을 요청하거나 답을 끝내요:curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5-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" } ] } ] }'ant messages create <<'YAML' model: claude-opus-5-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 YAMLclient = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5-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)const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5-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);var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5_5, 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);client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5_5, 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)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_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()); }use Anthropic\Messages\ToolBash20250124; $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5-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);client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5-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.contentstop_reason이tool_use인 동안 실행·반환 주기를 반복해요. 전체 루프는 클라이언트 도구 결과 처리하기를 참고하세요. -
안전 조치 구현하기 — 검증과 제한을 추가해요. 차단 목록보다 허용 목록을 사용하세요. 차단 목록은 예상하지 못한 명령을 놓치기 때문이에요. 예시는 별도 단어로 나타나는 셸 연산자도 거부해요:
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, Noneconst 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 }; }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); }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, "" }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); }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, '"\''); 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]; }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이 확인은 명백한 실수에 대한 트립와이어이지, 강제 경계가 아니에요. 이 페이지의 다른 예시들이 사용하는 띄어쓰기 체이닝(
&&), 파이프, 리다이렉션을 거부해요.cat data.txt|grep x처럼 단어에 붙은 연산자는 잡아내지 못해요. 토크나이저가data.txt|grep을 한 토큰 안에 두기 때문이에요. 애플리케이션이 허용할 명령과 연산자를 결정하세요. 진짜 통제는 격리예요. 전체 세션을 컨테이너나 가상 머신 안에서 실행하세요. (보안 참고)
오류 처리하기 (Handle errors)
명령이 실패하거나 세션이 깨지면 Claude에 무슨 일이 있었는지 알려주세요. 메시지를 tool_result 콘텐츠로 반환하고 is_error를 true로 설정해 도구 호출이 실패했음을 표시해요. is_error로 오류 처리하기를 참고하세요.
명령 실행 타임아웃 (Command execution timeout): 명령 실행이 너무 오래 걸리면:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "Error: command did not finish within 30 seconds",
"is_error": true
}
]
}
명령을 찾을 수 없음 (Command not found): 명령이 존재하지 않으면:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "bash: nonexistentcommand: command not found",
"is_error": true
}
]
}
권한 거부 (Permission denied): 권한 문제가 있으면:
{
"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)
명령 타임아웃 사용하기 (Use command timeouts):
결코 끝나지 않는 명령(입력을 기다리는 명령 등)은 센티널 줄이 도착하지 않으므로 세션을 영원히 막아요. 모든 명령에 마감 시간을 주세요. 마감 시간이 지나면 셸과 명령이 시작한 모든 것을 중지한 다음 세션을 재시작해요:
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"
// 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);
}
}
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";
}
// 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)
}
}
// 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();
}
}
// 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";
}
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
kill은 멈춘 명령과 그것이 시작한 모든 것을 중지해요. 메시지를 오류 tool_result로 반환하세요 (오류 처리하기 참고). 그러면 도구 호출이 실패로 표시돼요.
세션 상태 유지하기 (Maintain session state):
환경 변수와 작업 디렉터리를 유지하려면 bash 세션을 지속 상태로 유지하세요:
# Commands run in the same session maintain state
commands = [
"cd /tmp",
"echo 'Hello' > test.txt",
"cat test.txt", # The session is still in /tmp
]
// 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
];
// 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
];
// 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
}
// 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
);
// Commands run in the same session maintain state
$commands = [
'cd /tmp',
"echo 'Hello' > test.txt",
'cat test.txt', // The session is still in /tmp
];
# Commands run in the same session maintain state
commands = [
"cd /tmp",
"echo 'Hello' > test.txt",
"cat test.txt" # The session is still in /tmp
]
큰 출력 처리하기 (Handle large outputs):
토큰 한도 문제를 막으려면 큰 출력을 자릅니다:
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
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;
}
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;
}
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
}
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;
}
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;
}
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
모든 명령 로그 기록하기 (Log all commands):
감사 추적을 유지하세요. 모든 명령을 명령이 실행 전 기록하고 출력이 끝난 뒤 기록하는 하나의 래퍼로 라우팅하세요. 명령이 멈추거나 세션을 깨도 기록은 남아 있어요:
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
// 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;
}
// 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;
}
// 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
}
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;
}
// 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;
}
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
기록은 기본적으로 stderr로 가요. 파일이나 로깅 파이프라인으로 보내 보관하세요. 엔드 사용자와 tool_use_id처럼 기록을 요청과 연결하는 것을 애플리케이션에서 포함하세요.
보안 (Security)
경고 (Warning) 여러분의 애플리케이션은 Claude가 요청하는 어떤 명령이든 실행해요. 컨테이너나 가상 머신 같은 격리된 환경에서 최소 권한 사용자로 세션을 실행하세요. 모든 명령을 신뢰할 수 없는 입력으로 취급하세요.
격리 외에 다음 통제를 추가하세요:
- 실행 전에 차단 목록이 아닌 허용 목록으로 명령을 검증하세요. Bash 도구 구현하기를 참고하세요.
- 셸 프로세스에 리소스 한도(CPU, 메모리, 디스크)를 설정하세요(예:
ulimit사용). - 모든 명령과 출력을 기록해서 무엇이 실행됐는지 감사할 수 있게 하세요.
- Claude에 반환하기 전에 출력에서 자격 증명과 다른 비밀을 편집하세요.
요금 (Pricing)
bash 도구 정의는 요청에 다음 입력 토큰을 추가해요. 이는 어떤 도구든 있을 때 적용되는 모델별 도구 사용 시스템 프롬프트에 추가로 적용돼요.
| 모델 | 추가 입력 토큰 |
|---|---|
| Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7 | 325 토큰 |
| Claude Opus 4.6, Claude Sonnet 4.6 및 이전 | 244 토큰 |
다음에 의해 추가 토큰이 소비돼요:
- 명령 출력 (stdout/stderr)
- 오류 메시지
- 큰 파일 내용
전체 요금 세부 정보는 도구 사용 요금을 참고하세요.
일반적인 패턴 (Common patterns)
개발 워크플로우 (Development workflows):
- 테스트 실행:
pytest && coverage report - 프로젝트 빌드:
npm install && npm run build - Git 작업:
git status && git add . && git commit -m "message"
장기 실행 에이전트 워크플로우에서 checkpoint-and-recovery 메커니즘으로 git을 사용하는 지침은 상태 관리 모범 사례를 참고하세요.
파일 작업 (File operations):
- 데이터 처리:
wc -l *.csv && ls -lh *.csv - 파일 검색:
find . -name "*.py" | xargs grep "pattern" - 백업 생성:
tar -czf backup.tar.gz ./data
시스템 작업 (System tasks):
- 리소스 확인:
df -h && free -m - 프로세스 관리:
ps aux | grep python - 환경 설정:
export PATH=$PATH:/new/path && echo $PATH
제한 사항 (Limitations)
- 대화형 명령 불가: 세션은
vim,less, 비밀번호 프롬프트, stdin에서 입력을 기다리는 어떤 명령도 실행할 수 없어요. - GUI 애플리케이션 없음: 세션은 명령줄 전용이에요.
- 세션 범위: bash 세션 상태는 클라이언트 측이에요. 턴 사이에 셸 세션을 유지하는 책임은 여러분의 애플리케이션에게 있어요.
- 출력 한도: API는 도구 결과를 자르지 않아요 (과도하게 큰 요청은 거부돼요). Claude에 반환하기 전에 여러분의 애플리케이션에서 큰 출력을 자르세요.
- 스트리밍 없음: 출력은 여러분의 애플리케이션이 다음 요청에서
tool_result를 반환할 때만 Claude에 도달해요.
다른 도구와 결합하기 (Combining with other tools)
bash 도구는 Text editor 도구와 잘 어울려요. Claude는 한 도구로 파일을 편집하고 다른 도구로 그것을 실행하는 명령을 요청해요.
참고 (Note) 코드 실행 도구도 함께 쓰고 있다면 Claude는 두 개의 별도 실행 환경(여러분의 로컬 bash 세션과 Anthropic의 샌드박스 컨테이너)에 접근해요. 두 환경 사이에는 상태가 공유되지 않아요. 환경을 구분하도록 Claude에 프롬프트하는 지침은 다른 실행 도구와 함께 코드 실행 사용하기를 참고하세요.
더 알아보기 (Learn more)
- Text editor 도구 (Text editor tool) — 텍스트 파일을 보고 수정해서 코드를 디버깅, 수정, 개선하기
- Claude와 도구 사용 (Tool use with Claude) — Claude를 외부 도구와 API에 연결하기. 도구가 어디서 실행되는지, Claude가 언제 호출하는지, 어떤 도구가 작업에 맞는지 보기