시리즈 이전 편: [Part 5] 동적 프롬프트 분석
AI 모델은 기본적으로 대화만 할 수 있습니다. 텍스트를 받아서 텍스트를 돌려주는 것, 그게 전부입니다. 그런데 Claude Code는 파일을 읽고, 코드를 실행하고, 디렉토리를 탐색합니다. 어떻게 가능할까요?
답은 도구(Tool) 시스템입니다. 모델이 "이 파일을 읽어야겠다"고 판단하면 tool_use 블록을 출력하고, 호스트 프로그램이 해당 도구를 실행해서 결과를 다시 모델에게 돌려줍니다. 이번 편에서는 Claude Code가 40개 도구를 어떤 구조로 정의하고 관리하는지, 소스 코드를 직접 따라가 보겠습니다.
목차
- Tool 인터페이스 설계 — 모든 도구의 뼈대
- 40개 도구 카테고리별 정리
- 도구 하나의 구조 — FileReadTool 분석
- 도구 권한 체크 흐름
- 자신의 AI 앱에 도구 시스템을 추가하려면
- 정리
Tool 인터페이스 설계 — 모든 도구의 뼈대
도구의 타입 정의는 src/Tool.ts에 있습니다. 핵심 필드만 추리면 이렇습니다.
// src/Tool.ts (362행~)
export type Tool<Input, Output, P> = {
readonly name: string
inputSchema: Input // Zod 스키마
description(input, options): Promise<string>
prompt(options): Promise<string>
call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
checkPermissions(input, context): Promise<PermissionResult>
isReadOnly(input): boolean
isConcurrencySafe(input): boolean
isEnabled(): boolean
maxResultSizeChars: number
}
name은 모델이 호출할 때 사용하는 식별자이고, inputSchema는 Zod로 정의한 파라미터 스키마입니다. description은 모델에게 보여줄 도구 설명, prompt는 시스템 프롬프트에 삽입될 상세 지침입니다. call이 실제 실행 로직이고, checkPermissions가 실행 전 권한 검사를 담당합니다.
모든 도구는 직접 이 타입을 구현하지 않고, buildTool() 함수를 통해 생성됩니다.
// src/Tool.ts (783행~)
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
TOOL_DEFAULTS가 isEnabled: () => true, isReadOnly: () => false 같은 안전한 기본값을 채워 넣습니다. 개별 도구는 필요한 부분만 오버라이드하면 됩니다. 전형적인 빌더 패턴입니다.
40개 도구 카테고리별 정리
src/tools/ 디렉토리 아래 도구별 폴더가 40개 존재합니다. 카테고리별로 나누면 다음과 같습니다.
| 카테고리 | 도구 | 역할 |
|---|---|---|
| 파일 I/O | FileReadTool, FileWriteTool, FileEditTool, NotebookEditTool | 파일 읽기/쓰기/편집, 주피터 노트북 |
| 검색 | GlobTool, GrepTool, ToolSearchTool, LSPTool | 파일명 패턴, 내용 검색, 도구 검색, LSP |
| 실행 | BashTool, PowerShellTool, REPLTool | 셸 명령, PowerShell, REPL 실행 |
| 에이전트/협업 | AgentTool, SendMessageTool, SkillTool, TeamCreateTool, TeamDeleteTool | 서브에이전트 생성, 메시지, 스킬, 팀 |
| 태스크 관리 | TaskCreateTool, TaskGetTool, TaskListTool, TaskUpdateTool, TaskStopTool, TaskOutputTool | 백그라운드 태스크 CRUD |
| 상태/제어 | ConfigTool, TodoWriteTool, SleepTool, BriefTool, EnterPlanModeTool, ExitPlanModeTool | 설정, 할일, 대기, 모드 전환 |
| 외부 연동 | WebFetchTool, WebSearchTool, MCPTool, McpAuthTool, ListMcpResourcesTool, ReadMcpResourceTool | 웹 요청, MCP 서버 연동 |
| 워크트리/인프라 | EnterWorktreeTool, ExitWorktreeTool, RemoteTriggerTool, ScheduleCronTool, SyntheticOutputTool, AskUserQuestionTool | Git 워크트리, 원격 트리거, 크론 |
파일 I/O, 검색, 실행이 코딩 에이전트의 핵심 기능이고, 나머지는 협업과 상태 관리를 위한 도구들입니다.
도구 하나의 구조 — FileReadTool 분석
실제 도구가 어떻게 만들어지는지 FileReadTool을 예시로 보겠습니다. 경로는 src/tools/FileReadTool/FileReadTool.ts입니다.
// src/tools/FileReadTool/FileReadTool.ts (227행~)
const inputSchema = lazySchema(() =>
z.strictObject({
file_path: z.string().describe('The absolute path to the file to read'),
offset: semanticNumber(z.number().int().nonnegative().optional())
.describe('The line number to start reading from'),
limit: semanticNumber(z.number().int().positive().optional())
.describe('The number of lines to read'),
pages: z.string().optional()
.describe('Page range for PDF files'),
}),
)
입력 스키마를 lazySchema로 감싸서 지연 초기화합니다. Zod의 strictObject를 사용해 정의되지 않은 필드가 들어오면 거부합니다.
도구 본체는 buildTool로 생성합니다.
// src/tools/FileReadTool/FileReadTool.ts (337행~)
export const FileReadTool = buildTool({
name: FILE_READ_TOOL_NAME,
searchHint: 'read files, images, PDFs, notebooks',
maxResultSizeChars: Infinity,
strict: true,
async description() { return DESCRIPTION },
get inputSchema() { return inputSchema() },
isConcurrencySafe() { return true },
isReadOnly() { return true },
// ...
})
isReadOnly: true는 파일을 수정하지 않는다는 선언이고, isConcurrencySafe: true는 동시에 여러 번 호출해도 안전하다는 뜻입니다. 이 두 플래그가 병렬 실행 결정에 사용됩니다.
도구 권한 체크 흐름
Claude Code의 도구 실행은 무조건 권한 검사를 먼저 거칩니다. Tool.ts 주석을 보면 흐름이 명확합니다.
- validateInput — 입력값 자체가 유효한지 검사 (경로 존재 여부 등)
- checkPermissions — 도구별 권한 로직 실행 (파일시스템 접근 범위 등)
- 일반 권한 시스템 —
alwaysAllow,alwaysDeny,alwaysAsk규칙 매칭 - call — 모든 검사를 통과한 경우에만 실행
FileReadTool의 checkPermissions는 checkReadPermissionForTool을 호출해서, 읽으려는 파일이 허용된 경로 안에 있는지 확인합니다.
// src/tools/FileReadTool/FileReadTool.ts (398행~)
async checkPermissions(input, context): Promise<PermissionDecision> {
const appState = context.getAppState()
return checkReadPermissionForTool(
FileReadTool, input, appState.toolPermissionContext,
)
},
ToolPermissionContext에는 PermissionMode, alwaysAllowRules, alwaysDenyRules 같은 규칙이 담겨 있습니다. 사용자가 "이 도구는 항상 허용"으로 설정했는지, "이 경로는 항상 거부"인지를 여기서 판단합니다.
buildTool의 기본값은 checkPermissions: () => ({ behavior: 'allow', updatedInput: input })입니다. 즉, 별도로 구현하지 않으면 무조건 허용됩니다. 보안이 중요한 도구(Bash, FileWrite 등)만 명시적으로 오버라이드하는 구조입니다.
자신의 AI 앱에 도구 시스템을 추가하려면
Claude Code의 도구 설계에서 가져갈 수 있는 패턴을 정리하면 이렇습니다.
첫째, 스키마를 선언적으로 정의하세요. Zod 같은 라이브러리로 입력/출력 스키마를 정의하면, 모델에게 보낼 JSON Schema 변환과 런타임 검증을 한 번에 해결할 수 있습니다.
둘째, 권한 검사를 도구 실행과 분리하세요. checkPermissions와 call을 분리한 덕분에, 권한 정책을 도구 구현과 독립적으로 변경할 수 있습니다. 도구가 늘어나도 보안 정책은 한 곳에서 관리됩니다.
셋째, 메타데이터를 풍부하게 두세요. isReadOnly, isConcurrencySafe, isDestructive 같은 플래그가 병렬 실행, UI 표시, 안전성 판단에 두루 쓰입니다. 도구 수가 40개를 넘어가면 이런 메타데이터 없이는 시스템을 관리할 수 없습니다.
넷째, 빌더 패턴으로 기본값을 통일하세요. buildTool이 안전한 기본값(비활성 동시성, 쓰기 가능 가정)을 채워 넣기 때문에, 개발자가 깜빡 잊어도 fail-closed 방향으로 동작합니다.
정리
Claude Code의 도구 시스템은 결국 "AI 모델에게 현실 세계와 상호작용할 수 있는 인터페이스를 제공하는 것"입니다. 40개 도구 각각이 이름, 스키마, 설명, 권한, 실행 로직을 가진 하나의 독립된 모듈이고, buildTool이라는 팩토리 함수가 이 모듈들의 일관성을 보장합니다.
다음 편에서는 이 도구들의 프롬프트를 깊이 들여다봅니다. BashTool의 프롬프트가 왜 500줄이나 되는지, 모델에게 도구 사용법을 어떻게 가르치는지 분석합니다.
다음 편 예고: [Claude Code 해부학 Part 7] 클로드 코드(Claude Code) 도구 프롬프트 분석 — BashTool은 왜 500줄인가
'AI 코딩 에이전트' 카테고리의 다른 글
| Claude Code 멀티에이전트 분석: AI가 AI를 부리는 구조 (해부학 Part 9) (0) | 2026.04.01 |
|---|---|
| Claude Code QueryEngine 분석: AI와 대화하는 코드의 정체 (해부학 Part 8) (0) | 2026.04.01 |
| Claude Code 동적 프롬프트 분석: 매번 다르게 말하는 비밀 (해부학 Part 5) (0) | 2026.04.01 |
| Claude Code 프롬프트 캐싱 분석: API 비용을 90% 줄이는 구조 (해부학 Part 4) (0) | 2026.04.01 |
| Claude Code 시스템 프롬프트 분석: 915줄은 어떻게 조립되는가 (해부학 Part 3) (0) | 2026.04.01 |
