홈시리즈

© 2026 Ki Chang. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 Ki Chang. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

Claude Code 소스 유출이 드러낸 아키텍처 — 2편: 에이전틱 루프, 도구, 권한의 삼중주

정기창·2026년 4월 5일

들어가며

1편에서 Claude Code의 전체 조감도를 살펴봤습니다. 34개 서브시스템, 1,902개 파일이라는 규모감을 확인했지만, 이것만으로는 "실제로 어떻게 동작하는가"를 이해하기 어렵습니다.

이번 편에서는 Claude Code의 심장부에 해당하는 에이전틱 루프를 파헤쳐 보겠습니다. 사용자가 프롬프트를 입력하면 그때부터 벌어지는 일 — 모델이 도구를 호출하고, 권한을 확인하고, 훅을 실행하고, 결과를 다시 모델에 넘기는 순환 구조가 어떻게 설계되어 있는지를 살펴봅니다.

ConversationRuntime — 무한 에이전틱 루프

Claude Code의 핵심 엔진은 QueryEngine.ts(원본)이며, claw-code의 Rust 포트에서는 ConversationRuntime이라는 이름으로 재구현되어 있습니다. 이 엔진의 본질은 놀라울 정도로 단순합니다.

사용자 입력
    ↓
┌─────────────────────────────────────┐
│  1. 사용자 메시지를 세션에 추가       │
│  2. Claude API 호출 (스트리밍)        │
│  3. 응답에서 tool_use 블록 추출       │
│  4. 각 도구에 대해:                   │
│     ├── Permission 체크              │
│     ├── PreToolUse 훅 실행           │
│     ├── 도구 실행                    │
│     └── PostToolUse 훅 실행          │
│  5. tool_result를 세션에 추가         │
│  6. tool_use가 있으면 → 2번으로      │
│  7. 없으면 (end_turn) → 사용자에게    │
└─────────────────────────────────────┘
    ↓
사용자 출력

핵심은 6번입니다. 모델이 도구를 호출하는 한, 루프는 계속 돌아갑니다. 사용자의 추가 입력 없이도 모델이 자율적으로 여러 도구를 연속 호출하며 작업을 진행할 수 있습니다.

Rust 포트에서 이 루프의 최대 반복 횟수는 max_iterations: usize::MAX로 설정되어 있습니다. 사실상 무한입니다. 이론적으로는 모델이 end_turn을 보내지 않는 한 영원히 실행될 수 있다는 뜻입니다.

이것이 Claude Code가 복잡한 작업을 수행할 수 있는 근본적인 이유입니다. "파일을 읽고 → 코드를 분석하고 → 수정하고 → 테스트를 실행하고 → 실패하면 다시 수정하고 → 커밋한다"는 일련의 과정이, 하나의 루프 안에서 모델의 판단에 따라 자동으로 이어지는 것입니다.

자동 압축 트리거

무한 루프라면 컨텍스트가 끝없이 커지지 않을까요? 이를 방지하기 위해 자동 압축 임계값이 설정되어 있습니다.

DEFAULT_AUTO_COMPACTION_INPUT_TOKENS_THRESHOLD: 200,000 토큰

입력 토큰이 200K를 초과하면 자동으로 대화를 요약하여 컨텍스트를 줄입니다. 이 압축 메커니즘은 3편에서 자세히 다루겠습니다.

184개 도구 — 에이전트의 손과 발

에이전틱 루프가 심장이라면, 도구(Tools)는 에이전트의 손과 발입니다. 아키텍처 스냅샷에는 184개의 도구가 등록되어 있습니다.

도구 카테고리 분류

카테고리도구 예시설명
파일 작업FileReadTool, FileWriteTool, FileEditTool파일 CRUD
검색GlobTool, GrepTool파일 시스템 검색
웹WebSearchTool, WebFetchTool웹 검색/페치
실행BashTool, PowerShellTool셸 명령 실행
에이전트AgentTool (6개 빌트인)서브에이전트 생성
MCPMCPTool, McpAuthTool, ListMcpResourcesToolMCP 서버 연동
태스크TaskCreateTool, TaskGetTool, TaskUpdateTool백그라운드 태스크
팀TeamCreateTool, TeamDeleteTool멀티에이전트 팀
유틸리티ToolSearchTool, SkillTool, NotebookEditTool도구 검색, 스킬 실행

도구 등록 패턴

각 도구는 생성 시 자신에게 필요한 최소 권한 수준을 선언합니다. Rust 포트에서 이 패턴이 명확하게 드러납니다.

pub fn mvp_tool_specs() -> Vec<ToolSpec> {
    vec![
        ToolSpec { name: "bash",        required_permission: DangerFullAccess },
        ToolSpec { name: "read_file",   required_permission: ReadOnly },
        ToolSpec { name: "write_file",  required_permission: WorkspaceWrite },
        ToolSpec { name: "edit_file",   required_permission: WorkspaceWrite },
        ToolSpec { name: "glob_search", required_permission: ReadOnly },
        ToolSpec { name: "grep_search", required_permission: ReadOnly },
        ToolSpec { name: "WebFetch",    required_permission: ReadOnly },
        ToolSpec { name: "WebSearch",   required_permission: ReadOnly },
    ]
}

이 설계가 흥미로운 이유는, 도구 자체가 자신의 위험도를 알고 있다는 점입니다. bash는 스스로 "나는 위험한 도구"라고 선언하고, read_file은 "나는 안전한 도구"라고 선언합니다. 런타임은 이 선언과 현재 권한 모드를 비교하여 허용 여부를 결정합니다.

도구 풀 어셈블리

184개 도구가 항상 모두 로드되는 것은 아닙니다. Python 포트의 tool_pool.py에서 ToolPool이라는 개념이 등장합니다. 이는 현재 세션에서 활성화된 도구들의 집합입니다.

ToolPool은 다음 기준으로 도구를 필터링합니다.

  • primitiveTools: 항상 로드되는 기본 도구 (Read, Write, Edit, Bash, Glob, Grep 등)
  • preapproved: 사전 승인된 도구 (설정에 따라)
  • MCP 도구: 연결된 MCP 서버의 도구 (동적 로딩)
  • deferred 도구: ToolSearch로 필요할 때만 로드

이것이 바로 Claude Code 사용 시 일부 도구가 ToolSearch를 통해 "검색 후 로드"되는 이유입니다. 모든 도구의 스키마를 매 API 호출마다 전송하면 토큰 소비가 엄청나기 때문에, 자주 쓰는 기본 도구만 시스템 프롬프트에 포함하고 나머지는 지연 로딩하는 전략입니다.

5단계 Permission 모델 — 자율과 안전의 균형

에이전틱 루프가 무한히 도구를 호출할 수 있다면, 안전장치가 없으면 위험할 수밖에 없습니다. Claude Code는 이를 5단계 권한 모델로 해결합니다.

모드수준허용 범위
ReadOnly최저파일 읽기, 검색만
WorkspaceWrite중간+ 파일 쓰기/편집
DangerFullAccess높음+ Bash 실행, 위험 작업
Prompt동적매번 사용자에게 물어봄
Allow최고모든 것 자동 허용

권한 판단 로직

Rust 포트에서 권한 판단 로직을 의사코드로 정리하면 다음과 같습니다.

authorize(도구명, 입력, 프롬프터):
    현재_모드 = 활성화된 권한 모드
    요구_모드 = 도구가 선언한 필요 권한

    if 현재_모드 == Allow:
        → 자동 허용

    if 현재_모드 >= 요구_모드:
        → 자동 허용
        (예: DangerFullAccess 모드에서 ReadOnly 도구 = 자동 허용)

    if 현재_모드 == Prompt:
        → 사용자에게 물어봄

    if 현재_모드 == WorkspaceWrite && 요구_모드 == DangerFullAccess:
        → 사용자에게 물어봄
        (예: 쓰기 모드에서 Bash 실행 시도 = 사용자 판단)

    그 외:
        → 거부 ("insufficient permission")

이 로직에서 핵심적인 설계 결정이 보입니다.

첫째, 상위 호환입니다. DangerFullAccess 모드는 ReadOnly와 WorkspaceWrite 도구를 자동으로 포함합니다. 권한 수준이 계층적으로 설계되어 있는 것입니다.

둘째, Prompt 모드의 위치입니다. Prompt는 수준 자체가 아니라 "판단을 사용자에게 위임하는" 동적 모드입니다. 매번 물어보기 때문에 가장 안전하면서도 가장 불편한 모드입니다.

셋째, 경계 지점이 명확합니다. WorkspaceWrite에서 DangerFullAccess로 넘어가는 순간에만 사용자 확인이 발생합니다. 파일을 읽고 쓰는 것은 허용하되, 임의의 셸 명령을 실행하는 것은 별도 확인을 받겠다는 것입니다. 이것이 바로 Claude Code에서 Bash 명령을 실행할 때 확인 프롬프트가 뜨는 이유입니다.

사용자 경험과의 매핑

이 5단계 모델은 실제 사용 경험과 정확히 매핑됩니다.

  • 파일을 읽을 때 (Read) → 확인 없이 자동 실행
  • 파일을 편집할 때 (Edit) → 기본 모드에서는 확인, 자동 허용 설정 시 자동
  • npm install을 실행할 때 (Bash) → 거의 항상 사용자 확인
  • rm -rf 같은 위험 명령 → 시스템 프롬프트에서 이미 경고

돌이켜 생각해보면, 매일 Claude Code를 사용하면서 "왜 이건 물어보고 저건 안 물어보지?"라고 느꼈던 경험이 이 코드에서 설명됩니다. 단순한 규칙 기반이 아니라, 도구 선언 + 모드 비교 + 프롬프터라는 3요소가 결합된 설계인 것입니다.

Hook 시스템 — 도구 실행의 감시자

권한 모델이 "이 도구를 실행해도 되는가?"를 판단한다면, 훅 시스템은 "실행 전후에 무엇을 할 것인가?"를 결정합니다.

훅 이벤트 타입

Claude Code의 훅은 두 가지 시점에 실행됩니다.

PreToolUse   → 도구 실행 "전"에 실행
PostToolUse  → 도구 실행 "후"에 실행

훅의 실행 방식 — Git Hooks와 같은 패턴

흥미로운 것은 훅이 외부 셸 커맨드로 실행된다는 점입니다. Claude Code 내부의 코드가 아니라, 사용자가 등록한 셸 스크립트가 실행됩니다.

훅 실행 흐름:

1. 환경변수 설정
   ├── HOOK_EVENT = "PreToolUse" 또는 "PostToolUse"
   ├── HOOK_TOOL_NAME = "bash" 등 도구 이름
   └── HOOK_TOOL_INPUT = JSON 형식의 도구 입력

2. JSON stdin으로 추가 컨텍스트 전달

3. 셸 스크립트 실행

4. exit code로 판정
   ├── exit 0   → 허용 (Allow)
   ├── exit 2   → 거부 (Deny) — 도구 실행 차단
   └── 기타     → 경고 (Warn) — 경고 메시지 후 계속

이 패턴은 Git hooks와 완전히 동일합니다. Git의 pre-commit 훅이 exit 0이면 커밋을 허용하고, 비정상 종료하면 커밋을 차단하는 것과 같은 원리입니다.

실제로 저는 .claude/hooks/branch-protection.sh라는 훅을 사용하고 있는데, 이 훅이 내부적으로 어떻게 실행되는지 이제 정확히 이해됩니다. Claude Code가 도구를 호출할 때마다 이 셸 스크립트를 별도 프로세스로 실행하고, exit code를 확인하여 허용 여부를 결정하는 것입니다.

원본의 훅 104개 모듈

원본 TypeScript에서 hooks/ 서브시스템은 104개 모듈로 구성되어 있습니다. 이 중 상당수는 React 훅(useState, useEffect 등)이지만, tool permission 관련 핸들러들이 특히 주목할 만합니다.

hooks/
├── toolPermission/
│   ├── interactiveHandler.ts      # 대화형 권한 핸들러
│   ├── coordinatorHandler.ts      # 코디네이터 모드 핸들러
│   └── swarmWorkerHandler.ts      # Swarm 워커 핸들러
├── notifs/
│   ├── rateLimitNotif.ts          # Rate limit 알림
│   ├── mcpConnectivityNotif.ts    # MCP 연결 상태 알림
│   └── pluginNotif.ts             # 플러그인 알림
└── unifiedSuggestions.ts          # 통합 제안 시스템

coordinatorHandler.ts와 swarmWorkerHandler.ts의 존재가 흥미롭습니다. 일반 대화 모드, 코디네이터 모드(에이전트 조율), Swarm 모드(다수 에이전트)에서 각각 다른 권한 핸들러가 동작한다는 뜻입니다. 즉, 에이전트의 역할에 따라 권한 판단 로직이 달라집니다.

비용 추적 — costHook과 CostTracker

에이전틱 루프가 무한히 돌 수 있다면, 비용도 무한히 증가할 수 있습니다. Claude Code는 이를 costHook.ts와 cost-tracker.ts라는 두 모듈로 추적합니다.

모델별 가격 구조

Rust 포트에서 모델별 가격이 하드코딩되어 있습니다.

항목OpusSonnetHaiku
입력 (per 1M tokens)$15$15$1
출력 (per 1M tokens)$75$75$5
캐시 생성 (per 1M)$18.75-$1.25
캐시 읽기 (per 1M)$1.50-$0.10

토큰 사용량 추적 구조

struct TokenUsage {
    input_tokens: u32,                  // 일반 입력 토큰
    output_tokens: u32,                 // 출력 토큰
    cache_creation_input_tokens: u32,   // 캐시 생성 토큰
    cache_read_input_tokens: u32,       // 캐시 읽기 토큰
}

네 가지 토큰 유형이 별도로 추적됩니다. 캐시 생성과 캐시 읽기가 분리되어 있다는 점이 주목할 만합니다. Claude의 프롬프트 캐싱 기능에서 캐시를 새로 만드는 비용과 기존 캐시를 읽는 비용이 다르기 때문입니다.

costHook.ts는 루트 레벨에 위치한 파일로, 매 API 호출마다 비용을 기록합니다. 이 훅이 에이전틱 루프의 매 턴마다 실행되어, 사용자가 현재 세션의 비용을 실시간으로 확인할 수 있게 합니다. Claude Code에서 /cost 명령어를 실행하면 보이는 그 화면의 데이터 소스인 것입니다.

시스템 프롬프트 빌더

에이전틱 루프에서 모델에게 전달되는 시스템 프롬프트도 단순한 문자열이 아닙니다. Rust 포트의 prompt.rs에서 시스템 프롬프트를 동적으로 조립하는 로직이 드러납니다.

시스템 프롬프트 구성 요소:

1. 기본 지침 (역할, 도구 사용법, 안전 규칙)
2. 환경 정보 (OS, 셸, 작업 디렉토리, git 상태)
3. CLAUDE.md 내용 (프로젝트별 지침)
4. 활성화된 도구 스키마 (primitive + MCP)
5. 세션 메모리 (auto memory 내용)
6. 현재 날짜/시간
7. 모드별 추가 지침 (coordinator, agent 등)

이것이 CLAUDE.md에 작성한 프로젝트 지침이 시스템 프롬프트의 일부로 주입되는 메커니즘입니다. 1편에서 "CLAUDE.md는 시스템 초기화의 구성 요소"라고 했던 것의 구체적인 구현이 여기에 있습니다.

도구 스키마도 주목할 부분입니다. primitive 도구의 전체 스키마가 매 API 호출마다 시스템 프롬프트에 포함됩니다. MCP 도구의 경우, 연결된 모든 서버의 도구 스키마가 추가됩니다. 이것이 MCP 서버를 많이 연결할수록 토큰 소비가 증가하는 이유입니다 — 매 턴마다 모든 도구의 JSON Schema가 시스템 프롬프트에 들어가기 때문입니다.

실행 레지스트리 — 커맨드와 도구의 이중 구조

Python 포트의 execution_registry.py에서 흥미로운 구분이 드러납니다. Claude Code 내부에서 "실행 가능한 것"은 커맨드와 도구라는 두 가지 축으로 나뉩니다.

구분커맨드 (207개)도구 (184개)
실행 주체사용자 (슬래시 명령)모델 (에이전틱 루프)
트리거/commit, /doctor 등모델의 tool_use 응답
분류builtin / plugin / skillprimitive / mcp / deferred
권한사용자가 직접 실행하므로 암묵적 허용Permission 모델에 의한 판단

커맨드는 사용자가 의도적으로 실행하는 것이므로 별도의 권한 확인이 필요 없지만, 도구는 모델이 자율적으로 호출하는 것이므로 Permission 모델을 거쳐야 합니다. 이 구분이 Claude Code의 안전성 설계에서 핵심적인 역할을 합니다.

커맨드의 분류도 흥미롭습니다. command_graph.py에서 커맨드는 세 카테고리로 세그먼트됩니다.

  • builtin: Claude Code에 내장된 명령 (/help, /cost, /doctor 등)
  • plugin: 플러그인이 제공하는 명령
  • skill: 스킬이 제공하는 명령 (/commit, /review-pr 등)

2편을 마치며 — 세 가지의 조화

에이전틱 루프, 도구 시스템, 권한 모델 — 이 세 가지는 서로 긴밀하게 엮여 있습니다.

에이전틱 루프는 자율성을 제공합니다. 모델이 스스로 판단하여 필요한 도구를 호출하고, 결과를 보고 다음 행동을 결정합니다.

도구 시스템은 능력을 제공합니다. 184개의 도구가 파일 읽기부터 웹 검색, MCP 연동까지 다양한 행동을 가능하게 합니다.

권한 모델과 훅은 제어를 제공합니다. 5단계 권한 수준, 도구별 위험도 선언, 셸 기반 훅으로 자율 실행의 범위를 세밀하게 조절합니다.

이 세 가지의 균형이 Claude Code의 핵심 설계 철학을 드러냅니다 — "최대한 자율적으로 동작하되, 위험한 순간에는 반드시 사람의 판단을 받는다."

다음 편에서는 이 에이전틱 루프가 장시간 실행될 때 필수적인 메모리와 컨텍스트 관리를 살펴보겠습니다. 4계층 메모리 시스템, 200K 토큰 자동 압축 알고리즘, 그리고 "자기 치유 메모리"라는 흥미로운 설계를 다룹니다.

시리즈 안내
1편: 512,000줄의 전체 조감도
2편: 에이전틱 루프 — 도구, 권한, 훅의 삼중주 (현재 글)
3편: 메모리와 컨텍스트 — 잊지 않는 AI의 비밀
4편: Bridge, MCP, 멀티 에이전트 — 단순 CLI를 넘어서

Claude Code에이전틱 루프AI 에이전트권한 모델Hook도구 시스템

관련 글

Claude Code 소스 유출이 드러낸 아키텍처 — 1편: 512,000줄의 전체 조감도

2026년 3월 31일, npm 패키징 실수로 Claude Code의 TypeScript 소스 512,000줄이 유출되었습니다. 1,902개 파일, 34개 서브시스템, 207개 커맨드, 184개 도구 — 이 숫자들이 말해주는 AI 코딩 에이전트의 내부 구조를 조감도로 살펴봅니다.

관련도 94%

Claude Code 소스 유출이 드러낸 아키텍처 — 4편: Bridge, MCP, 멀티 에이전트

Claude Code는 단독 CLI가 아닙니다. 31개 모듈의 Bridge가 IDE와 연결하고, 5종 트랜스포트의 MCP가 외부 도구를 통합하고, 6개 빌트인 에이전트가 팀으로 협업합니다. 마지막 편에서는 이 '연결의 아키텍처'를 살펴봅니다.

관련도 94%

Claude Code 소스 유출이 드러낸 아키텍처 — 3편: 메모리와 컨텍스트 압축

AI 에이전트가 장시간 작업할 때 가장 큰 적은 컨텍스트 한계입니다. Claude Code는 4계층 메모리, 200K 토큰 자동 압축, 자기 치유 메모리, 지연 로딩이라는 네 가지 전략으로 이 문제를 해결합니다.

관련도 93%