---
title: Bash 도구
url: https://platform.claude.com/docs/ko/agents-and-tools/tool-use/bash-tool
description: Claude가 셸 명령을 요청하면 애플리케이션이 지속적인 bash 세션에서 이를 실행하고 도구 결과로 반환하도록 합니다.
---

<Note>
  "zero data retention"(제로 데이터 보존), 즉 ZDR이 이 기능에 어떻게 적용되는지 알아보려면 [API 및 데이터 보존](https://platform.claude.com/docs/ko/manage-claude/api-and-data-retention)을 참조하세요.
</Note>

bash 도구는 [클라이언트 도구](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/how-tool-use-works)입니다. Claude는 명령을 직접 실행하지 않습니다. 요청에 이 도구를 포함하면 Claude는 실행할 명령을 지정하는 `tool_use` 블록으로 응답합니다. 애플리케이션은 자체적으로 소유한 bash 세션에서 해당 명령을 실행하고 출력을 `tool_result` 블록으로 반환합니다.

애플리케이션은 도구 호출 전반에 걸쳐 하나의 bash 프로세스를 유지하므로 명령 간에 상태가 지속됩니다. 작업 디렉터리, 환경 변수, 그리고 명령이 생성한 모든 파일은 다음 명령에서도 그대로 남아 있습니다.

이 도구의 현재 버전은 `bash_20250124`입니다. 모델 지원, 베타 헤더 및 이전 버전에 대해서는 [도구 버전](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/bash-tool#tool-versions)을 참조하세요. Anthropic이 제공하는 모든 도구에 대해서는 [도구 레퍼런스](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/tool-reference)를 참조하세요.

## 사용 사례

* **개발 워크플로:** 빌드 명령, 테스트 및 개발 도구 실행
* **시스템 자동화:** 스크립트 실행, 파일 관리, 작업 자동화
* **데이터 처리:** 파일 처리, 분석 스크립트 실행, 데이터셋 관리
* **환경 설정:** 패키지 설치, 환경 구성

## 빠른 시작

<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-5",
      "max_tokens": 1024,
      "tools": [
        {
          "type": "bash_20250124",
          "name": "bash"
        }
      ],
      "messages": [
        {
          "role": "user",
          "content": "List all Python files in the current directory."
        }
      ]
    }'
  ```

  ```bash CLI
  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.}'
  ```

  ```python Python
  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)
  ```

  ```typescript TypeScript
  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);
  ```

  ```csharp C#
  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);
  ```

  ```go Go
  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)
  ```

  ```java Java
  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);
  }
  ```

  ```php PHP
  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;
  ```

  ```ruby Ruby
  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
  ```
</CodeGroup>

Claude는 `stop_reason: "tool_use"`와 함께 애플리케이션이 실행할 명령이 담긴 `tool_use` 블록으로 응답합니다:

```json Output
{
  "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"
      }
    }
  ]
}
```

bash 세션에서 `input.command`를 실행하고 출력을 `tool_result`로 다시 보내세요. 왕복 과정에 대해서는 [bash 도구 구현](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/bash-tool#implement-the-bash-tool)을 참조하세요.

## 작동 방식

각 도구 호출은 Claude와 애플리케이션 간의 한 번의 왕복입니다:

1. Claude가 실행할 `command`가 담긴 `tool_use` 블록을 반환합니다.
2. 애플리케이션이 자체 bash 세션에서 명령을 실행합니다.
3. 애플리케이션이 명령의 출력(stdout과 stderr를 함께)을 `tool_result` 블록으로 Claude에 반환합니다.
4. Claude는 같은 세션에서 다른 명령을 요청하거나 텍스트로 응답합니다.

Claude는 하나의 응답에서 여러 개의 `tool_use` 블록을 반환할 수도 있습니다. 같은 세션에서 순서대로 실행하고 모든 결과를 하나의 `user` 메시지로 반환하세요. [병렬 도구 사용](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/parallel-tool-use)을 참조하세요.

API는 상태를 저장하지 않습니다(stateless). 셸 세션에 관한 어떤 것도 요청 간에 전달되지 않으므로, 세션이 언제 시작되고 얼마나 오래 유지되며 언제 재시작할지는 애플리케이션이 결정합니다. 전체 요청 및 응답 주기에 대해서는 [도구 호출 처리](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/handle-tool-calls)를 참조하세요.

## 매개변수

bash 도구 정의에는 `type`과 `name`이라는 두 개의 필수 필드가 있으며, `name`은 반드시 `bash`여야 합니다. 이 도구는 스키마가 없습니다. 스키마가 Claude 모델에 내장되어 있어 수정할 수 없으므로 `input_schema`를 제공하지 않습니다. 다음 표는 Claude가 도구를 호출할 때 설정하는 입력 필드를 나열합니다.

| 매개변수      | 필수  | 설명                         |
| --------- | --- | -------------------------- |
| `command` | 예\* | 실행할 bash 명령                |
| `restart` | 아니요 | bash 세션을 재시작하려면 `true`로 설정 |

\*`restart`를 사용하지 않는 한 필수

`restart: true`를 처리하려면 셸 프로세스를 종료하고 새 프로세스를 시작한 다음, 재시작을 확인하는 `tool_result`를 반환하세요. 재시작된 세션은 깨끗한 상태로 시작됩니다. 작업 디렉터리, 환경 변수 및 실행 중이던 모든 프로세스가 사라집니다.

<Accordion title="사용 예시">
  명령 실행:

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

  세션 재시작:

  ```json
  {
    "restart": true
  }
  ```
</Accordion>

## 도구 버전

`bash_20250124`는 이 도구의 현재 버전이며 베타 헤더가 필요하지 않습니다. Claude Sonnet 3.7([종료](https://platform.claude.com/docs/ko/about-claude/model-deprecations)) 이후의 모든 모델이 이를 지원하며, 현재의 모든 Claude 모델도 포함됩니다.

원래의 `bash_20241022` 버전은 2024년 10월 Claude Sonnet 3.5 모델([종료](https://platform.claude.com/docs/ko/about-claude/model-deprecations))에서만 작동합니다. 이 버전을 사용하는 요청에는 `anthropic-beta: computer-use-2024-10-22` 헤더가 필요하며, SDK는 베타 네임스페이스에서만 이를 제공합니다. 새로운 통합에서는 `bash_20250124`를 사용해야 합니다.

## 예시: 다단계 자동화

Claude는 도구 호출 전반에 걸쳐 명령을 연결하여 다단계 작업을 완료할 수 있습니다:

```text
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 도구 구현

Claude는 어떤 명령을 실행할지 결정합니다. 그 외의 모든 것, 즉 셸 프로세스, 타임아웃, 안전 검사는 애플리케이션이 담당합니다. 다음 단계는 최소한의 구현을 보여줍니다.

<Steps>
  <Step title="지속적인 bash 세션 생성">
    수명이 긴 bash 프로세스 하나를 시작하고 모든 명령을 그 안에서 실행하세요. 살아 있는 프로세스에 연결된 파이프는 파일 끝(end-of-file)을 보고하지 않으므로, 세션은 각 명령 뒤에 고유한 센티널(sentinel) 줄을 출력하여 해당 명령의 출력이 끝나는 지점을 표시합니다:

    <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";

      // 명령 간에 상태가 유지되도록 계속 살아 있는 bash 프로세스입니다.
      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 });
        }

        // 세션에서 명령을 실행하고 그 출력을 반환합니다.
        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.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

      // 명령 간에 상태가 유지되도록 계속 살아 있는 bash 프로세스입니다.
      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;
          }

          // 세션에서 명령을 실행하고 그 출력을 반환합니다.
          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은 명령 간에 살아 있어 상태가 유지되는 bash 프로세스입니다.
      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는 세션에서 명령을 실행하고 그 출력을 반환합니다.
      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는 셸을 종료하고 그 자리에 새 세션을 시작합니다.
      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;

      // 명령 간에 상태가 유지되도록 계속 살아 있는 bash 프로세스입니다.
      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()));
          }

          // 세션에서 명령을 실행하고 그 출력을 반환합니다.
          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
      // 명령 간에 상태가 유지되도록 계속 살아 있는 bash 프로세스입니다.
      class BashSession
      {
          public $process;
          public $stdin;
          public $output;

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

          private function start(): void
          {
              // setsid는 셸에 자체 프로세스 그룹을 부여합니다: 타임아웃 시 모든 자식 프로세스를 종료할 수 있습니다
              $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];
          }

          // 세션에서 명령을 실행하고 그 출력을 반환합니다.
          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"

      # 명령 간에 살아 있어 상태가 유지되는 bash 프로세스입니다.
      class BashSession
        attr_reader :output, :wait_thread

        def initialize
          start
        end

        # 세션에서 명령을 실행하고 그 출력을 반환합니다.
        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는 오류를 출력과 순서대로 섞어 내보내며, pgroup은 셸에 자체
          # 프로세스 그룹을 부여하여 타임아웃 시 모든 자식 프로세스를 종료할 수 있게 합니다
          @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>

    세션은 stderr를 stdout과 섞어 출력하므로 오류 메시지가 발생한 위치에 나타납니다. 이 예시에는 완전한 구현에 필요한 것이 빠져 있습니다. 명령이 멈췄을 때 셸과 셸이 시작한 모든 프로세스를 종료한 다음 세션을 재시작하는 타임아웃입니다. [명령 타임아웃 사용](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/bash-tool#follow-implementation-best-practices) 모범 사례에서 이를 추가하는 한 가지 방법을 보여줍니다.
  </Step>

  <Step title="Claude의 도구 호출 처리">
    Claude의 응답에서 명령을 추출하고 실행하세요:

    <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)

              # tool_use 블록마다 tool_result 하나씩, 모두 다음 user 메시지에 담아 반환
              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 ?? "");
          }

          // tool_use 블록마다 tool_result 하나씩, 모두 다음 user 메시지에 담아 반환합니다
          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);
              }

              // tool_use 블록마다 tool_result 하나씩, 모두 다음 user 메시지에 담아 반환합니다
              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)
      		}

      		// tool_use 블록마다 tool_result 하나씩, 모두 다음 user 메시지에 담아 반환
      		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);
              }

              // tool_use 블록마다 tool_result 하나씩, 모두 다음 user 메시지에 담아 반환합니다
              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']);
              }

              // tool_use 블록마다 tool_result 하나씩, 모두 다음 user 메시지에서 반환
              $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

        # tool_use 블록마다 tool_result 하나씩, 모두 다음 user 메시지에 담아 반환
        tool_results << {type: "tool_result", tool_use_id: block.id, content: result}
      end
      ```
    </CodeGroup>
  </Step>

  <Step title="Claude에 결과 반환">
    같은 대화를 이어가는 `user` 메시지에 `tool_result`를 담아 다시 보내세요. Claude는 같은 세션에서 다른 명령을 요청하거나 답변을 마무리합니다:

    <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-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-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-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-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_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);
      ```

      ```go Go
      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)
      ```

      ```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_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-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-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>

    `stop_reason`이 `tool_use`인 동안 실행 및 반환 주기를 반복하세요. 전체 루프에 대해서는 [클라이언트 도구의 결과 처리하기](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/handle-tool-calls#handling-results-from-client-tools)를 참조하세요.
  </Step>

  <Step title="안전 조치 구현하기">
    검증과 제한을 추가하세요. 차단 목록(blocklist) 대신 허용 목록(allowlist)을 사용하세요. 차단 목록은 예상하지 못한 명령을 모두 놓치게 됩니다. 이 예시는 별도의 단어로 나타나는 셸 연산자도 거부합니다:

    <CodeGroup exclude="shell">
      ```python Python
      import shlex

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


      def validate_command(command):
          # 명시적 허용 목록에 있는 명령만 허용합니다
          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"

          # 별도의 단어로 작성된 셸 연산자는 거부합니다
          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 } {
        // 공백 기준으로 분할: 트립와이어 검사에는 충분합니다
        const tokens = command.split(/\s+/).filter((token) => token.length > 0);
        if (tokens.length === 0) {
          return { ok: false, reason: "Empty command" };
        }

        // 명시적 허용 목록에 있는 명령만 허용합니다
        const executable = tokens[0];
        if (!ALLOWED_COMMANDS.has(executable)) {
          return { ok: false, reason: `Command '${executable}' is not in the allowlist` };
        }

        // 별도 단어로 작성된 셸 연산자는 거부합니다
        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)
      {
          // 공백 기준으로 분할: 트립와이어 검사에는 충분합니다
          var tokens = command.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
          if (tokens.Length == 0)
          {
              return (false, "Empty command");
          }

          // 명시적 허용 목록에 있는 명령만 허용합니다
          var executable = tokens[0];
          if (!allowedCommands.Contains(executable))
          {
              return (false, $"Command '{executable}' is not in the allowlist");
          }

          // 별도 단어로 작성된 셸 연산자는 거부합니다
          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) {
      	// 공백 기준으로 분할: 트립와이어 검사에는 충분합니다
      	tokens := strings.Fields(command)
      	if len(tokens) == 0 {
      		return false, "Empty command"
      	}

      	// 명시적 허용 목록에 있는 명령만 허용합니다
      	executable := tokens[0]
      	if !allowedCommands[executable] {
      		return false, fmt.Sprintf("Command %q is not in the allowlist", executable)
      	}

      	// 별도 단어로 작성된 셸 연산자는 거부합니다
      	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) {
          // 공백 기준으로 분할: 트립와이어 검사에는 충분합니다
          List<String> tokens = List.of(command.trim().split("\\s+"));
          if (tokens.size() == 1 && tokens.get(0).isEmpty()) {
              return new Validation(false, "Empty command");
          }

          // 명시적 허용 목록에 있는 명령만 허용합니다
          String executable = tokens.get(0);
          if (!ALLOWED_COMMANDS.contains(executable)) {
              return new Validation(false, "Command '" + executable + "' is not in the allowlist");
          }

          // 별도 단어로 작성된 셸 연산자는 거부합니다
          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
      {
          // 공백 기준으로 분할: 트립와이어 검사에는 충분함
          $tokens = preg_split('/\\s+/', trim($command), -1, PREG_SPLIT_NO_EMPTY);
          if ($tokens === false || $tokens === []) {
              return [false, 'Empty command'];
          }

          // 명시적 허용 목록에 있는 명령만 허용
          $executable = $tokens[0];
          if (!in_array($executable, ALLOWED_COMMANDS, true)) {
              return [false, "Command '{$executable}' is not in the allowlist"];
          }

          // 별도 단어로 작성된 셸 연산자는 거부
          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)
        # 명시적 허용 목록에 있는 명령만 허용
        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

        # 별도 단어로 작성된 셸 연산자 거부
        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>

    이 검사는 명백한 실수를 잡아내는 경보 장치일 뿐, 강제 경계가 아닙니다. 이 페이지의 다른 예시에서 사용하는 공백으로 구분된 연결(`&&`), 파이프, 리디렉션을 거부합니다. `cat data.txt|grep x`처럼 단어에 붙어 있는 연산자는 잡아내지 못하는데, 토크나이저가 `data.txt|grep`을 하나의 토큰 안에 유지하기 때문입니다. 애플리케이션이 어떤 명령과 연산자를 허용할지 결정하세요. 실질적인 통제 수단은 격리입니다. 전체 세션을 컨테이너나 가상 머신 안에서 실행하세요([보안](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/bash-tool#security) 참조).
  </Step>
</Steps>

### 오류 처리

명령이 실패하거나 세션이 깨지면 무슨 일이 일어났는지 Claude에 알려주세요. 메시지를 `tool_result` 콘텐츠로 반환하고 `is_error`를 `true`로 설정하면 해당 도구 호출이 실패한 것으로 표시됩니다. [is\_error로 오류 처리](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/handle-tool-calls#handling-errors-with-is-error)를 참조하세요.

<AccordionGroup>
  <Accordion title="명령 실행 타임아웃">
    명령 실행에 너무 오랜 시간이 걸리는 경우:

    ```json
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
          "content": "Error: command did not finish within 30 seconds",
          "is_error": true
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="명령을 찾을 수 없음">
    명령이 존재하지 않는 경우:

    ```json
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
          "content": "bash: nonexistentcommand: command not found",
          "is_error": true
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="권한 거부">
    권한 문제가 있는 경우:

    ```json
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
          "content": "bash: /root/sensitive-file: Permission denied",
          "is_error": true
        }
      ]
    }
    ```
  </Accordion>
</AccordionGroup>

### 구현 모범 사례 따르기

<AccordionGroup>
  <Accordion title="명령 타임아웃 사용">
    입력을 기다리는 명령처럼 끝나지 않는 명령은 센티널 줄이 도착하지 않기 때문에 세션을 영원히 차단합니다. 모든 명령에 기한을 부여하세요. 기한이 지나면 셸과 명령이 시작한 모든 것을 중지한 다음 세션을 재시작하세요:

    <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:
                  # 그룹은 셸과 해당 명령이 시작한 모든 프로세스를 포함합니다
                  os.killpg(session.process.pid, signal.SIGKILL)
                  session.restart()
                  return f"Error: command did not finish within {timeout} seconds"
      ```

      ```typescript TypeScript
      // 세션에서 명령을 실행하고, 명령이 멈추면 세션을 교체합니다.
      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 {
          // 그룹은 셸과 명령이 시작한 모든 프로세스입니다
          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;

      // 세션에서 명령을 실행하고, 명령이 멈추면 세션을 교체합니다.
      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;
          }

          // 셸과 셸이 시작한 모든 프로세스를 중지한 다음 새 세션을 시작합니다
          session.Process.Kill(entireProcessTree: true);
          session.Restart();
          return $"Error: command did not finish within {timeoutSeconds} seconds";
      }
      ```

      ```go Go
      // executeWithTimeout은 명령을 실행하며, 명령이 멈추면 세션을 교체합니다.
      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):
      		// 그룹은 셸과 해당 명령이 시작한 모든 프로세스입니다
      		syscall.Kill(-session.cmd.Process.Pid, syscall.SIGKILL)
      		session.Restart()
      		return fmt.Sprintf("Error: command did not finish within %d seconds", timeoutSeconds)
      	}
      }
      ```

      ```java Java
      // 세션에서 명령을 실행하고, 명령이 멈추면 세션을 교체합니다.
      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) {
              // 셸과 셸이 시작한 모든 프로세스를 중지한 다음 새 세션을 시작합니다
              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
      // 명령을 실행하되 기한 내에 끝나지 않으면 포기합니다. PHP는 파이프 읽기에서
      // 블로킹되므로 기한 검사는 읽기 루프 안에 있습니다. stream_select()가 각 fgets() 전에
      // 읽을 수 있는 출력을 기다리므로 루프에서 기한을 확인할 수 있습니다.
      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;
          }

          // 그룹은 셸과 명령이 시작한 모든 프로세스입니다
          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"

      # 세션에서 명령을 실행하고, 명령이 멈추면 세션을 교체합니다.
      def execute_with_timeout(session, command, timeout: 30)
        Timeout.timeout(timeout) { session.execute_command(command) }
      rescue Timeout::Error
        # 그룹은 셸과 해당 명령이 시작한 모든 프로세스입니다
        Process.kill("KILL", -session.wait_thread.pid)
        session.restart
        "Error: command did not finish within #{timeout} seconds"
      end
      ```
    </CodeGroup>

    kill은 멈춘 명령과 그 명령이 시작한 모든 것을 중지합니다. 메시지를 오류 `tool_result`로 반환하세요([오류 처리](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/bash-tool#handle-errors) 참조). 이렇게 하면 해당 도구 호출이 실패한 것으로 표시됩니다.
  </Accordion>

  <Accordion title="세션 상태 유지">
    환경 변수와 작업 디렉터리를 유지하려면 bash 세션을 지속적으로 유지하세요:

    <CodeGroup exclude="shell">
      ```python Python
      # 동일한 세션에서 실행되는 명령은 상태를 유지합니다
      commands = [
          "cd /tmp",
          "echo 'Hello' > test.txt",
          "cat test.txt",  # The session is still in /tmp
      ]
      ```

      ```typescript TypeScript
      // 동일한 세션에서 실행되는 명령은 상태를 유지합니다
      const commands = [
        "cd /tmp",
        "echo 'Hello' > test.txt",
        "cat test.txt" // The session is still in /tmp
      ];
      ```

      ```csharp C#
      // 동일한 세션에서 실행되는 명령은 상태를 유지합니다
      string[] commands =
      [
          "cd /tmp",
          "echo 'Hello' > test.txt",
          "cat test.txt", // The session is still in /tmp
      ];
      ```

      ```go Go
      // 동일한 세션에서 실행되는 명령은 상태를 유지합니다
      commands := []string{
      	"cd /tmp",
      	"echo 'Hello' > test.txt",
      	"cat test.txt", // The session is still in /tmp
      }
      ```

      ```java Java
      // 동일한 세션에서 실행되는 명령은 상태를 유지합니다
      List<String> commands = List.of(
          "cd /tmp",
          "echo 'Hello' > test.txt",
          "cat test.txt" // The session is still in /tmp
      );
      ```

      ```php PHP
      // 동일한 세션에서 실행되는 명령은 상태를 유지합니다
      $commands = [
          'cd /tmp',
          "echo 'Hello' > test.txt",
          'cat test.txt', // The session is still in /tmp
      ];
      ```

      ```ruby Ruby
      # 동일한 세션에서 실행되는 명령은 상태를 유지합니다
      commands = [
        "cd /tmp",
        "echo 'Hello' > test.txt",
        "cat test.txt" # The session is still in /tmp
      ]
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="대용량 출력 처리">
    토큰 한도 문제를 방지하기 위해 대용량 출력을 잘라내세요:

    <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>
  </Accordion>

  <Accordion title="모든 명령 로깅">
    감사 추적을 유지하세요. 모든 명령을 하나의 래퍼를 통해 전달하여 실행 전에 명령을 기록하고 완료 후에 출력을 기록하세요. 멈추거나 세션을 깨뜨리는 명령도 기록을 남깁니다:

    <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
      // 세션에서 명령을 실행하고 해당 명령의 감사 기록을 남깁니다.
      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#
      // 세션에서 명령을 실행하고 해당 명령의 감사 기록을 남깁니다.
      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는 세션에서 명령을 실행하고 해당 명령의 감사 기록을 보관합니다.
      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");

      // 세션에서 명령을 실행하고 해당 명령의 감사 기록을 남깁니다.
      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
      // 세션에서 명령을 실행하고 해당 명령의 감사 기록을 남깁니다.
      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)

      # 세션에서 명령을 실행하고 해당 감사 기록을 보관합니다.
      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>

    기록은 기본적으로 `stderr`로 전송됩니다. 보관하려면 파일이나 로깅 파이프라인으로 보내세요. 최종 사용자나 `tool_use_id`처럼 애플리케이션에서 기록을 요청과 연결해 주는 정보를 포함하세요.
  </Accordion>
</AccordionGroup>

## 보안

<Warning>
  애플리케이션은 Claude가 요청하는 모든 명령을 실행합니다. 컨테이너나 가상 머신 같은 격리된 환경에서, 작업을 수행할 수 있는 최소 권한 사용자로 세션을 실행하세요. 모든 명령을 신뢰할 수 없는 입력으로 취급하세요.
</Warning>

격리 외에도 다음 통제 수단을 추가하세요:

* 명령을 실행하기 전에 차단 목록이 아닌 허용 목록으로 검증하세요. [bash 도구 구현](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/bash-tool#implement-the-bash-tool)을 참조하세요.
* 예를 들어 `ulimit`을 사용하여 셸 프로세스에 리소스 제한(CPU, 메모리, 디스크)을 설정하세요.
* 무엇이 실행되었는지 감사할 수 있도록 모든 명령과 그 출력을 로깅하세요.
* Claude에 반환하기 전에 출력에서 자격 증명 및 기타 비밀 정보를 삭제하세요.

## 가격

bash 도구 정의는 요청에 다음과 같은 입력 토큰을 추가합니다. 이는 도구가 하나라도 존재할 때마다 적용되는 모델별 [도구 사용 시스템 프롬프트](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/overview#pricing)에 추가되는 것입니다.

| 모델                                       | 추가 입력 토큰 |
| ---------------------------------------- | -------- |
| Claude 4.7 이상 모델 및 Claude Mythos Preview | 325 토큰   |
| Claude 4.6 이하 모델                         | 244 토큰   |

추가 토큰은 다음에 의해 소비됩니다:

* 명령 출력 (stdout/stderr)
* 오류 메시지
* 대용량 파일 내용

전체 가격 세부 정보는 [도구 사용 가격](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/overview#pricing)을 참조하세요.

## 일반적인 패턴

### 개발 워크플로

* 테스트 실행: `pytest && coverage report`
* 프로젝트 빌드: `npm install && npm run build`
* Git 작업: `git status && git add . && git commit -m "message"`

장기 실행 에이전트 워크플로에서 git을 체크포인트 및 복구 메커니즘으로 사용하는 방법에 대한 안내는 [상태 관리 모범 사례](https://platform.claude.com/docs/ko/build-with-claude/prompt-engineering/claude-prompting-best-practices#state-management-best-practices)를 참조하세요.

### 파일 작업

* 데이터 처리: `wc -l *.csv && ls -lh *.csv`
* 파일 검색: `find . -name "*.py" | xargs grep "pattern"`
* 백업 생성: `tar -czf backup.tar.gz ./data`

### 시스템 작업

* 리소스 확인: `df -h && free -m`
* 프로세스 관리: `ps aux | grep python`
* 환경 설정: `export PATH=$PATH:/new/path && echo $PATH`

## 제한 사항

* **대화형 명령 불가:** 세션은 `vim`, `less`, 비밀번호 프롬프트 또는 stdin에서 입력을 기다리는 어떤 명령도 실행할 수 없습니다.
* **GUI 애플리케이션 불가:** 세션은 명령줄 전용입니다.
* **세션 범위:** Bash 세션 상태는 클라이언트 측에 있습니다. 턴 사이에 셸 세션을 유지하는 것은 애플리케이션의 책임입니다.
* **출력 한도:** API는 도구 결과를 잘라내지 않습니다(크기가 초과된 요청은 거부됩니다). Claude에 반환하기 전에 애플리케이션에서 대용량 출력을 잘라내세요.
* **스트리밍 불가:** 출력은 애플리케이션이 다음 요청에서 `tool_result`를 반환할 때에만 Claude에 도달합니다.

## 다른 도구와 결합

bash 도구는 [텍스트 편집기 도구](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/text-editor-tool)와 잘 어울립니다. Claude는 한 도구로 파일을 편집하고 다른 도구로 이를 실행하는 명령을 요청합니다.

<Note>
  [코드 실행 도구](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/code-execution-tool)도 함께 사용하는 경우, Claude는 두 개의 별도 실행 환경, 즉 로컬 bash 세션과 Anthropic의 샌드박스 컨테이너에 접근할 수 있습니다. 두 환경 간에 상태는 공유되지 않습니다. Claude가 환경을 구분하도록 프롬프트하는 방법에 대한 안내는 [다른 실행 도구와 함께 코드 실행 사용](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/code-execution-tool#using-code-execution-with-other-execution-tools)을 참조하세요.
</Note>

## 다음 단계

<CardGroup cols={2}>
  <Card title="텍스트 편집기 도구" icon="file" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/text-editor-tool">
    코드를 디버그하고 수정하고 개선하기 위해 텍스트 파일을 보고 수정합니다.
  </Card>

  <Card title="Claude와 함께하는 도구 사용" icon="tool" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/overview">
    Claude를 외부 도구 및 API에 연결합니다. 도구가 어디서 실행되는지, Claude가 언제 도구를 호출하는지, 어떤 도구가 작업에 적합한지 알아보세요.
  </Card>
</CardGroup>
