Claude Code 가 도구를 발견, 전달, 실행하는 방법에 대한 심층 분석
소개
이전 글에서 우리는 유출된 소스 코드를 통해 Anthropic Harness 의 메모리 시스템을 리버스 엔지니어링했습니다. 메모리는 놀라울 정도로 단순했습니다 — 마크다운 파일, 프론트매터, 프롬프트 엔지니어링이 전부였죠. 도구 시스템은 그 반대입니다. 이는 전체 코드베이스에서 가장 정교하게 설계된 서브시스템입니다: 43 개 이상의 도구, 스트리밍 실행 파이프라인, 계층형 권한 시스템, 훅 프레임워크, 동시성 스케줄러 — 이 모든 것이 유기적으로 연결되어 상태 비저장 언어 모델이 파일을 읽고, 셸 명령을 실행하고, 웹을 검색하고, 하위 에이전트를 생성할 수 있는 무언가로 변모시킵니다.
이 글에서는 도구의 생애주기, 즉 도구가 어떻게 정의되는지부터 모델의 도구 호출이 어떻게 전달되고, 결과가 대화에 어떻게 다시 흘러 들어가는지까지 살펴봅니다. 광범위하게, 시스템은 네 가지 계층으로 구성됩니다: 모든 도구가 구현하는 도구 인터페이스, 도구 풀을 조립하는 레지스트리, 각 호출의 유효성을 검사하고 권한을 확인하고 실행하는 전달 파이프라인, 그리고 무엇을 병렬로 실행할지 결정하는 동시성 스케줄러.
아키텍처 한눈에 보기

도구 인터페이스
Claude Code 의 모든 도구는 Tool.ts 에 정의된 동일한 인터페이스를 구현합니다. 타입은 Input (Zod 스키마), Output (결과 타입), P (진행 데이터) 세 가지 매개변수에 대해 제네릭입니다. 실제로 도구는 약 30 개의 메서드를 가진 객체이지만, 시스템을 이해하는 데 중요한 것은 소수에 불과합니다.
핵심 형태:
1type Tool<Input, Output, P> = {2 name: string3 inputSchema: ZodType // 입력 검증을 위한 Zod 스키마4 call(input, context, canUseTool,5 parentMessage, onProgress): Promise<ToolResult>67 // 동작 선언8 isConcurrencySafe(input): boolean // 병렬 실행 가능?9 isReadOnly(input): boolean // 읽기 전용 작업?10 isDestructive(input): boolean // 파괴적 작업?1112 // 권한 및 검증13 checkPermissions(input, context): Promise<PermissionResult>14 validateInput(input, context): Promise<ValidationResult>1516 // API 통합17 description(input, options): Promise<string>18 prompt(options): Promise<string> // 이 도구에 대한 시스템 프롬프트 텍스트19 mapToolResultToToolResultBlockParam(result, toolUseId): ToolResultBlockParam2021 // UI 렌더링 (React)22 renderToolUseMessage(input, options): ReactNode23 renderToolResultMessage(content, ...): ReactNode24}
어떤 도구도 이것을 처음부터 구현하지 않습니다. buildTool() 이라는 팩토리 함수가 안전한 기본값을 채워줍니다:

기본값은 의도적으로 보수적입니다. 동시성 안전을 선언하는 것을 잊은 도구 작성자는 직렬 실행을 받게 됩니다. 권한 확인 구현을 잊은 도구 작성자는 기본 권한 흐름을 받게 됩니다. 시스템은 실패 시 폐쇄적으로 작동합니다.
ToolResult 타입도 주목할 만합니다:
1type ToolResult<T> = {2 data: T // 실제 출력3 newMessages?: Message[] // 선택적 후속 메시지4 contextModifier?: (ctx) => ToolUseContext // 다음 도구를 위해 컨텍스트 변경5 mcpMeta?: { ... } // MCP 프로토콜 메타데이터6}
contextModifier 는 중요합니다 — 도구가 같은 턴에서 후속 도구의 실행 컨텍스트를 변경할 수 있게 해줍니다. 이것이 EnterWorktree 같은 도구가 이후 모든 작업의 작업 디렉토리를 변경하는 방식입니다. 중요하게도, 컨텍스트 수정자는 동시성 안전하지 않은 도구에만 허용됩니다. 도구가 병렬로 실행되면 공유 상태를 수정할 수 없습니다.
도구 레지스트리
모든 도구는 tools.ts 의 단일 함수 getAllBaseTools() 에 등록됩니다. 이 함수는 평면 배열을 반환합니다. 일부 도구는 항상 존재하고, 다른 도구는 기능 플래그, 환경 변수 또는 플랫폼 확인 뒤에 게이트됩니다.
항상 사용 가능한 도구 (16 개)

기능 플래그로 게이트된 도구 (~27 개)
나머지 도구는 조건부로 포함됩니다. 일부는 환경 변수 뒤에 게이트되고 (USER_TYPE=ant 는 config 및 tungsten 과 같은 Anthropic 내부 도구용). 일부는 Statsig 를 통한 기능 플래그 뒤에 게이트됩니다 (web_browser, sleep, monitor). 일부는 플랫폼별입니다 (Windows 의 powershell). 일부는 복합 조건 뒤에 게이트됩니다 — repl 도구는 USER_TYPE=ant 와 REPL 기능 플래그가 모두 필요합니다.
기능 플래그로 게이트된 도구 전체 목록
Ant 전용: config, tungsten, suggest_background_pr, repl (REPL 플래그도 필요)
기능 플래그: web_browser, web_search, sleep, monitor, overflow_test, ctx_inspect, terminal_capture, list_peers, workflow, snip
에이전트 트리거: cron_create, cron_delete, cron_list, remote_trigger
Kairos (프로액티브 에이전트): sleep, send_user_file, push_notification, subscribe_pr
멀티 에이전트 스웜: team_create, team_delete, send_message
Todo v2: task_create, task_get, task_update, task_list
환경: lsp (ENABLE_LSP_TOOL), enter_worktree / exit_worktree (워크트리 모드), powershell (Windows)
도구 검색: tool_search (도구 풀이 클 때)
테스트 전용: testing_permission (NODE_ENV=test)
MCP 도구
내장 도구 외에도 Claude Code 는 Model Context Protocol (MCP) 서버를 지원합니다 — 표준화된 프로토콜을 통해 자체 도구를 노출하는 외부 프로세스입니다. MCP 도구는 연결된 서버에서 런타임에 동적으로 등록되며 동일한 Tool 인터페이스로 래핑됩니다. 전달 파이프라인의 관점에서 MCP 도구는 내장 도구와 구별할 수 없습니다.
각 MCP 도구는 원본 서버에 대한 메타데이터(mcpInfo: { serverName, toolName })를 전달하며, 이는 권한 규칙, 오류 처리 및 인증에 사용됩니다. MCP 도구가 인증 오류로 실패하면 시스템은 자동으로 서버 상태를 needs-auth 로 업데이트하고 사용자에게 문제를 표시합니다.
도구 풀 어셈블리
세 가지 함수가 최종 도구 세트를 조립합니다:
- getAllBaseTools() — 기능 게이트가 적용된 43 개 이상의 내장 도구의 원시 목록 반환
- getTools(permissionContext) — 거부 규칙 및 isEnabled() 로 필터링
- assembleToolPool(permissionContext, mcpTools) — 내장 도구와 MCP 도구 병합
assembleToolPool() 의 병합 전략은 의도적입니다:
내장 도구가 먼저 오므로, 이름 충돌 시 내장 도구가 우선합니다. 각 파티션 내 알파벳 정렬은 세션 간 순서를 안정적으로 유지하며, 이는 프롬프트 캐싱에 중요합니다 — 도구 배열은 API 요청의 일부이며, 재정렬하면 캐시가 손상됩니다.
assembleToolPool() 의 병합 전략은 의도적입니다:
1// 각 파티션을 알파벳순으로 정렬, 연결, 중복 제거2const byName = (a, b) => a.name.localeCompare(b.name)3return uniqBy(4 [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),5 'name',6)
API 직렬화
도구가 Claude API 에 도달하기 전에 toolToAPISchema() 가 각 도구의 Zod 스키마를 Anthropic API 의 JSON Schema 형식으로 변환합니다.
전달 파이프라인
Claude 가 응답할 때, 메시지에는 tool_use 블록 — 도구를 호출하는 구조화된 요청 — 이 포함될 수 있습니다. 전달 파이프라인은 이러한 블록을 7 단계로 처리합니다. 모든 도구 호출은 모든 단계를 순서대로 거칩니다.
1 단계: 추출
메인 쿼리 루프(query.ts)에서 tool_use 블록이 어시스턴트 메시지에서 필터링됩니다:
1const msgToolUseBlocks = message.message.content.filter(2 content => content.type === 'tool_use',3) as ToolUseBlock[]
각 블록에는 name, input 객체 및 고유 id 가 있습니다. id 는 중요합니다 — 도구 결과는 API 에 다시 보낼 때 동일한 id 를 참조해야 합니다. 그렇지 않으면 대화가 깨집니다.
2 단계: 입력 검증
도구의 Zod 스키마는 safeParse() 를 사용하여 원시 입력의 유효성을 검사합니다 — 유효한 데이터 또는 구조화된 오류를 반환하는 예외를 발생시키지 않는 변형입니다. 검증에 실패하면 모델은 스키마 힌트와 함께 형식화된 오류 메시지를 받고, 해당 도구의 실행은 중지됩니다. 잘못된 입력에 대해서는 코드가 실행되지 않습니다.
1const parsedInput = tool.inputSchema.safeParse(input)2if (!parsedInput.success) {3 let errorContent = formatZodValidationError(tool.name, parsedInput.error)4 // 오류를 모델에 반환, 실행 건너뜀5}
Zod 검증 후, 일부 도구는 스키마로 표현할 수 없는 의미론적 검증을 위해 두 번째 validateInput() 검사를 실행합니다. 예를 들어, 파일 경로가 상대 경로가 아닌 절대 경로인지 확인하는 경우입니다.
3 단계: 사전 도구 훅
권한 확인 전에 사용자 구성 훅이 실행됩니다. 이는 도구 호출 시 실행되는 외부 셸 명령 또는 스크립트입니다. 사전 도구 훅은 다음을 수행할 수 있습니다:
- 허용 — 대화형 권한 프롬프트를 우회하여 도구 호출 허용
- 거부 — 도구 호출을 완전히 거부
- 입력 수정 — 실행 전에 입력 수정
- 오류 메시지로 차단 — 도구 실행 차단
- 추가 컨텍스트 제공 — 사용자에게 추가 정보 제공
중요한 불변성: 훅의 allow 는 설정의 거부 규칙을 우회하지 않습니다. 소스 코드에는 이에 대한 명시적인 주석이 있습니다: "Hook 'allow' does NOT bypass settings.json deny/ask rules." 의도는 훅이 문을 열 수는 있지만, 자물쇠를 무시할 수는 없다는 것입니다.
4 단계: 권한 확인
권한 시스템은 파이프라인에서 가장 복잡한 부분입니다. 여러 계층을 통해 순서대로 해결됩니다:
- 거부 규칙 — 먼저 확인됩니다. 일치하는 거부 규칙이 있으면 실행이 즉시 중지됩니다. 거부 규칙은 최종적이며 다른 어떤 계층으로도 재정의될 수 없습니다.
- 질문 규칙 — 일치하면 사용자에게 승인을 요청합니다 (Bash 에 대해 샌드박스 자동 허용이 적용되지 않는 한).
- 도구별 권한 — 도구 자체의 checkPermissions() 메서드가 실행됩니다. 예를 들어 BashTool 은 명령을 파싱하여 서브 명령 수준 규칙을 확인합니다.
- 안전 검사 — 민감한 경로(.git/, .claude/, 셸 설정 파일)에 대한 하드코딩된 보호 조치입니다. 이는 우회 면역입니다 — 완전 우회 모드에서도 대화형 승인이 필요합니다.
- 권한 모드 — 사용자의 구성된 모드가 기본 동작을 결정합니다.
- 허용 규칙 — 마지막으로 확인됩니다. 허용 규칙이 일치하고 거부/질문 규칙이 트리거되지 않으면 도구가 진행됩니다.
권한 모드
default — "질문" 결정에 대해 항상 사용자에게 프롬프트를 표시합니다.
acceptEdits — 안전한 파일 작업(읽기, 편집)은 자동 허용, 그 외 모든 경우 프롬프트 표시.
bypassPermissions — 거부 규칙 및 안전 검사를 제외한 모든 것을 자동 허용.
plan — 먼저 계획을 승인한 다음, 이전 모드를 따라 실행.
auto — AI 분류기를 사용하여 허용 또는 프롬프트 여부를 결정.
dontAsk — 모든 "질문" 결정을 "거부"로 변환 — 프롬프트 없이 거절.
권한 규칙은 여러 소스에서 오며 우선 순위 순서로 해결됩니다: policySettings, localSettings, projectSettings, userSettings, flagSettings, cliArg, command, session. 이를 통해 조직 정책이 사용자 기본 설정을 재정의하고 CLI 인수가 둘 다 재정의할 수 있습니다.
5 단계: 실행
권한이 부여되면 도구의 call() 메서드가 호출됩니다:
1const result = await tool.call(2 callInput,3 { ...toolUseContext, toolUseId: toolUseID },4 canUseTool,5 assistantMessage,6 progress => onToolProgress({ toolUseID: progress.toolUseID, data: progress.data })7)
다섯 가지 인수: 검증된 입력, 실행 컨텍스트(작업 디렉토리, 중단 컨트롤러, 앱 상태), 권한 콜백(중간에 추가 권한이 필요한 도구용), 상위 어시스턴트 메시지, 실시간 업데이트를 위한 진행 콜백. 지속 시간은 전역적으로 추적됩니다.
미묘한 세부 사항: call() 에 전달된 입력은 훅과 권한이 본 백필된 버전이 아닌 모델의 원래 입력입니다. 이는 대화록 일관성을 유지합니다 — 대화에 기록된 도구 호출이 모델이 생성한 것과 정확히 일치합니다.
6 단계: 사후 도구 훅
실행 후 사후 도구 훅이 실행됩니다. 이는 MCP 도구 출력을 수정하거나, 추가 컨텍스트를 제공하거나, 대화가 계속되는 것을 차단할 수 있습니다. 또한 오류 발생 시에만 실행되는 별도의 PostToolUseFailure 훅이 있어 외부 시스템이 실패를 기록하거나 수정을 제안할 수 있습니다.
7 단계: 결과 매핑
각 도구는 mapToolResultToToolResultBlockParam() 을 구현하여 출력을 Anthropic API 의 ToolResultBlockParam 형식 — tool_use_id 참조와 문자열 또는 구조화된 콘텐츠가 포함된 tool_result 블록 — 으로 변환합니다.
결과가 크기 임계값을 초과하면 sessionDir/tool-results/{toolUseId}.txt 에 디스크에 저장되고 파일 참조가 포함된 미리보기가 대신 API 로 전송됩니다. 이는 대용량 출력(10,000 줄 파일 읽기, 자세한 명령 출력)이 대화 컨텍스트를 비대해지는 것을 방지합니다.
동시성 스케줄러
모델이 단일 메시지에서 여러 도구 호출을 내보낼 때, 모두 한 번에 실행되지는 않습니다. 스케줄러는 동시성 안전에 기반하여 도구 호출을 배치로 분할합니다.
알고리즘은 간단합니다. 도구 호출을 순서대로 살펴봅니다. 각각에 대해 isConcurrencySafe(input) 을 확인합니다. 안전하고 이전 배치도 안전했으면 배치에 추가합니다. 그렇지 않으면 새 배치를 시작합니다.
1// toolOrchestration.ts 에서 단순화2for (const toolUse of toolUseMessages) {3 const isSafe = tool.isConcurrencySafe(parsedInput)4 if (isSafe && lastBatch.isConcurrencySafe) {5 lastBatch.blocks.push(toolUse) // 동시 배치에 병합6 } else {7 batches.push({ isConcurrencySafe: isSafe, blocks: [toolUse] })8 }9}
안전한 배치는 동시에 실행됩니다 (최대 10 개 제한, CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 로 구성 가능). 안전하지 않은 배치는 직렬로 실행되며, 한 번에 하나의 도구씩 실행됩니다. 컨텍스트 수정자는 배치 간에만 적용되며 배치 내에서는 적용되지 않습니다.
실제로 이는 "이 5 개 파일 읽기"와 같은 메시지는 하나의 동시 배치를 생성하고, "이 파일 읽은 다음 편집"과 같은 메시지는 두 개의 직렬 배치를 생성함을 의미합니다. 모델은 단일 턴에서 두 패턴을 모두 트리거할 수도 있습니다 — 연속적인 읽기 전용 호출은 배치되고, 첫 번째 쓰기 호출은 배치를 중단합니다.
스트리밍 실행기
두 번째 실행 경로인 StreamingToolExecutor 가 있습니다. 스트리밍이 활성화되면 도구는 모델이 여전히 응답을 생성하는 동안 실행을 시작합니다. 스트림에서 각 tool_use 블록이 완료되면 전체 응답을 기다리지 않고 즉시 실행 대기열에 추가됩니다.
스트리밍 실행기는 동일한 동시성 규칙을 사용하지만 한 가지 동작을 추가합니다: Bash 오류 연쇄 중단. 동시에 실행 중인 형제 도구가 있는 상태에서 Bash 명령이 실패하면 실행기는 모든 형제 도구를 중단합니다. 그 이유는 실패한 Bash 명령이 다른 도구가 작동하는 컨텍스트를 무효화할 가능성이 높기 때문입니다 — 계속하면 시간이 낭비되고 혼란스러운 오류가 발생할 수 있습니다.
1if (isErrorResult && tool.block.name === BASH_TOOL_NAME) {2 this.hasErrored = true3 this.siblingAbortController.abort('sibling_error')4}
실제 예제
구체적으로 이해하기 위해 모델이 파일을 읽기로 결정했을 때 어떤 일이 발생하는지 추적해 보겠습니다. 모델이 다음을 내보냅니다:
1{2 "type": "tool_use",3 "id": "toolu_01XYZ",4 "name": "read",5 "input": { "file_path": "/src/index.ts" }6}
추출: query.ts 에서 어시스턴트 메시지 콘텐츠에서 이를 필터링합니다.
- 도구 조회: findToolByName(tools, "read") 가 FileReadTool 을 찾습니다.
- 입력 검증: Zod 가 { file_path: "/src/index.ts" } 를 z.object({ file_path: z.string(), offset: z.number().optional(), limit: z.number().optional(), pages: z.string().optional() }) 에 대해 파싱합니다. 통과합니다.
- 사전 도구 훅: 사용자 구성 훅이 실행됩니다. 입력을 수정하지 않습니다.
- 권한 확인: FileReadTool 의 checkPermissions() 가 checkReadPermissionForTool() 을 호출합니다. 읽기 도구는 일반적으로 대부분의 권한 모드에서 허용됩니다.
- 실행: FileReadTool.call() 이 파일을 읽고, 줄 번호를 적용하고 (cat -n 형식), PDF/이미지/노트북을 특수 사례로 처리합니다.
- 결과 매핑: 파일 내용은 "toolu_01XYZ" 를 참조하는 tool_result 블록이 됩니다.
- 반환: 결과는 사용자 메시지로 대화에 추가되고 다음 API 호출에서 전송됩니다.
FileReadTool 이 isConcurrencySafe: () => true 및 isReadOnly: () => true 를 선언하므로, 모델이 동일한 메시지에서 5 개의 읽기 호출을 내보냈다면 5 개 모두 병렬로 실행됩니다.
요약
도구 시스템은 Claude Code 의 실행 백본입니다. 모델의 의도 — 구조화된 tool_use 블록으로 표현됨 — 를 실제 기계 작업으로 전환하며, 모든 단계에서 검증, 권한 및 동시성 제어를 수행합니다.
설계는 계층적입니다: 보수적인 buildTool() 팩토리가 안전한 기본값을 보장하고, 기능 플래그가 있는 레지스트리가 사용 가능한 것을 제어하며, 7 단계 전달 파이프라인이 모든 호출의 유효성을 검사하고 권한을 확인하며, 동시성 스케줄러가 정확성을 유지하면서 병렬 처리를 최대화합니다. 스트리밍 실행기는 그 위에 성능 최적화를 추가합니다 — 도구는 모델이 생각을 마치기 전에 실행을 시작합니다.
메모리 시스템(5 개의 경로, 마크다운 파일 디렉토리 및 프롬프트 엔지니어링)과 비교하면 도구 시스템은 본격적인 런타임입니다. 파일 캐비닛과 운영 체제의 차이와 같습니다.
흥미로운 점
스케줄러로서의 모델
동시성 스케줄러는 반응적입니다 — 모델이 내보내는 것을 배치로 묶습니다. 그러나 실제 스케줄러는 모델 그 자체입니다. 시스템 프롬프트는 "모든 독립적인 도구 호출을 병렬로 수행"하고 "단일 Bash 호출과 &&를 사용하여 종속 명령을 연결"하도록 지시합니다. 런타임은 이를 신뢰합니다. 모델이 5 개의 읽기 후 쓰기를 내보내면 스케줄러는 읽기를 병렬화하고 쓰기를 직렬화합니다. 그러나 그 순서는 모델이 결정했습니다. 스케줄러는 모델의 계획을 강제할 뿐 자체 계획을 세우지 않습니다.
기본적으로 실패 시 폐쇄
가장 일관된 설계 원칙: 모든 것이 실패 시 폐쇄됩니다. 알 수 없는 도구? 오류. 잘못된 입력? 오류. 동시성 선언 없음? 직렬 실행. 권한 선언 없음? 사용자에게 질문. 기능 플래그 없음? 도구가 존재하지 않음. 이는 주요 사용자가 도구 이름을 환각하거나 입력을 잘못 구성할 수 있는 AI 모델인 시스템에게는 이례적입니다. 시스템은 모델의 실수를 수용하지 않고 차단하도록 설계되었습니다.
확장 지점으로서의 훅
훅 시스템(사전 도구, 사후 도구, 사후 실패)은 기본 확장 지점입니다. 조직이 정책을 시행하고(사전 훅의 거부 규칙), 로깅 시스템이 도구 사용을 캡처하고(사후 훅), CI/CD 파이프라인이 통합되는(실패 훅) 방식입니다. 중요한 것은 훅이 제한만 강화할 수 있을 뿐 완화할 수는 없다는 것입니다. 훅은 설정이 허용하는 도구를 거부할 수 있지만, 설정이 거부하는 도구를 허용할 수는 없습니다.
43 개의 도구, 1 개의 인터페이스
아마도 가장 인상적인 것은 균일성입니다. bash 명령, web_fetch, 하위 에이전트 생성, cron 작업 생성, 푸시 알림은 모두 동일한 30 개 메서드 인터페이스를 구현하고, 동일한 7 단계 파이프라인을 거치며, 동일한 권한 시스템을 따릅니다. 디스패처에는 특별한 경우가 없습니다. 복잡성은 개별 도구 구현과 권한 규칙에 있으며, 라우팅에 있지 않습니다.





