더 많은 글은 radarlog.kr에서.
Claude Code의 시스템 프롬프트는 하드코딩된 문자열 하나가 아니다. 12개 이상의 섹션이 조건부로 조립되고, 그 중간에 캐시 경계선이 있다.
1편에서 숨겨진 커맨드와 환경변수를 다뤘다. 2편에서는 Claude Code가 내부에서 어떻게 생각하는지 — 시스템 프롬프트가 어떻게 만들어지고, 프롬프트 캐시를 어떻게 지키고, 에이전트를 어떻게 지휘하는지를 소스코드에서 직접 확인한다.
시스템 프롬프트는 이렇게 조립된다
constants/prompts.ts에 getSystemPrompt() 함수가 있다. 이 함수가 반환하는 건 문자열이 아니라 문자열 배열이다. 각 요소가 시스템 프롬프트의 한 섹션이다.
export async function getSystemPrompt(
tools: Tools,
model: string,
additionalWorkingDirectories?: string[],
mcpClients?: MCPServerConnection[],
): Promise<string[]> {반환값의 구조를 보면 Claude Code가 어떤 순서로 "성격"을 입는지 알 수 있다.
return [
// --- 정적 콘텐츠 (캐시 가능) ---
getSimpleIntroSection(outputStyleConfig),
getSimpleSystemSection(),
getSimpleDoingTasksSection(),
getActionsSection(),
getUsingYourToolsSection(enabledTools),
getSimpleToneAndStyleSection(),
getOutputEfficiencySection(),
// === 경계선 ===
SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
// --- 동적 콘텐츠 (매 턴 변경 가능) ---
...resolvedDynamicSections, // 메모리, MCP, 언어 설정 등
]경계선 위의 섹션들은 모든 사용자에게 동일하다. "너는 Claude Code야", "도구는 이렇게 써", "출력은 간결하게 해" 같은 고정 규칙들. 경계선 아래는 사용자마다 다르다. CLAUDE.md 내용, MCP 서버 설정, 언어 설정 등.
이 경계선이 왜 중요한지는 바로 다음 섹션에서 설명한다.
프롬프트 캐시를 깨지 않기 위한 집착
소스코드에서 가장 인상적인 부분이다. Anthropic은 프롬프트 캐시 비용을 줄이기 위해 엔지니어링에 엄청난 공을 들였다.
SYSTEM_PROMPT_DYNAMIC_BOUNDARY라는 상수가 있다. 이 경계선은 시스템 프롬프트 배열에서 "여기까지는 글로벌 캐시 가능, 여기부터는 동적 콘텐츠"를 나눈다.
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
'__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'경계선 위의 정적 콘텐츠는 scope: 'global'로 캐시된다. 모든 사용자가 같은 캐시를 공유할 수 있다. 경계선 아래는 사용자별로 다르니까 캐시를 공유할 수 없다.
이게 왜 중요하냐면, Anthropic API에서 캐시 생성(cache_creation) 토큰은 비싸고 캐시 읽기(cache_read) 토큰은 싸다. 시스템 프롬프트의 정적 부분이 바뀌면 캐시가 깨지고, 다시 비싼 cache_creation 토큰이 발생한다.
소스코드에는 캐시 깨짐을 감지하는 전담 모듈까지 있다. promptCacheBreakDetection.ts에서 매 턴마다 시스템 프롬프트의 해시를 비교한다.
type PreviousState = {
systemHash: number
toolsHash: number
cacheControlHash: number
toolNames: string[]
perToolHashes: Record<string, number>
systemCharCount: number
model: string
fastMode: boolean
globalCacheStrategy: string
betas: string[]
// ...더 있다
}도구 스키마가 바뀌면 캐시가 깨진다. MCP 서버가 연결/해제되면 캐시가 깨진다. 모델이 바뀌면 캐시가 깨진다. 이걸 전부 추적하고, 어떤 이유로 캐시가 깨졌는지 로깅한다.
소스코드 주석에 이런 문장이 있다.
// The dynamic agent list was ~10.2% of fleet cache_creation tokens
에이전트 목록이 동적으로 바뀌는 게 전체 캐시 생성 토큰의 10.2%를 차지했다는 거다. 그래서 에이전트 목록을 시스템 프롬프트가 아니라 **대화 메시지에 첨부(attachment)**하는 방식으로 바꿨다. 시스템 프롬프트가 안 바뀌니까 캐시가 안 깨진다.
게임 서버에서 핫 패스의 allocation을 줄이는 것과 같은 집착이다. "10.2%를 줄이기 위해 아키텍처를 바꿨다"는 게 Anthropic이 비용 최적화에 얼마나 진심인지 보여준다.
날짜도 마찬가지다. 시스템 프롬프트에 오늘 날짜를 넣는데, 자정이 지나면 날짜가 바뀌면서 캐시가 깨진다. 그래서 날짜를 세션 시작 시점에 memoize하고, 자정 이후의 날짜 변경은 캐시를 깨지 않는 attachment 메시지로 처리한다.
// Memoized for prompt-cache stability — captures the date once at session start.
export const getSessionStartDate = memoize(getLocalISODate)출력 효율성 — Ant 직원과 일반 사용자가 다르다
시스템 프롬프트 중 getOutputEfficiencySection()이 흥미롭다. USER_TYPE이 ant(Anthropic 직원)인지 아닌지에 따라 완전히 다른 프롬프트가 들어간다.
일반 사용자에게는 짧은 지시가 간다.
Keep your text output brief and direct. Lead with the answer or action,
not the reasoning. Skip filler words, preamble, and unnecessary transitions.
Anthropic 내부 직원에게는 훨씬 세밀한 지시가 간다. "사용자가 자리를 비웠다 돌아왔다고 가정하고 써라", "약어나 코드네임을 쓰지 마라", "역피라미드 구조로 써라" 같은 구체적인 글쓰기 가이드가 들어 있다.
When making updates, assume the person has stepped away and lost the thread.
They don't know codenames, abbreviations, or shorthand you created along the way...
더 놀라운 건 Ant 빌드에만 들어가는 숫자 기반 길이 제한이다.
// Numeric length anchors — research shows ~1.2% output token reduction
// vs qualitative "be concise". Ant-only to measure quality impact first.
'Length limits: keep text between tool calls to ≤25 words.
Keep final responses to ≤100 words unless the task requires more detail.'"간결하게 써"보다 "25단어 이내로 써"가 출력 토큰을 1.2% 더 줄인다는 연구 결과를 바탕으로 한 것이다. Anthropic 내부에서 먼저 테스트하고, 품질에 문제가 없으면 일반 사용자에게도 적용할 계획으로 보인다.
코디네이터 모드 — AI가 AI를 지휘한다
CLAUDE_CODE_COORDINATOR_MODE=1을 설정하면 Claude Code의 동작이 완전히 바뀐다. Claude가 직접 코드를 수정하는 대신, 워커 에이전트를 생성하고 지휘하는 역할만 한다.
코디네이터의 시스템 프롬프트가 소스코드에 전문이 있다. 핵심을 요약하면 이렇다.
You are a coordinator. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
- Answer questions directly when possible
코디네이터가 쓸 수 있는 도구는 딱 3개다. Agent(워커 생성), SendMessage(워커에게 메시지), TaskStop(워커 중지). 파일을 읽거나 수정하는 도구는 없다.
워크플로우가 단계별로 정의되어 있다. Research → Synthesis → Implementation → Verification. 각 단계를 워커가 수행하고, 코디네이터는 결과를 종합한다.
| Phase | Who | Purpose |
|----------------|------------------|----------------------------------|
| Research | Workers (parallel) | Investigate codebase |
| Synthesis | You (coordinator) | Craft implementation specs |
| Implementation | Workers | Make targeted changes, commit |
| Verification | Workers | Test changes work |
가장 강조되는 규칙이 있다. "Never delegate understanding."
Never write "based on your findings, fix the bug" or
"based on the research, implement it." Those phrases push
synthesis onto the agent instead of doing it yourself.
코디네이터는 워커의 리서치 결과를 직접 이해하고, 구체적인 파일 경로와 라인 넘버를 포함한 스펙을 작성해서 워커에게 넘겨야 한다. "네가 찾은 걸 기반으로 고쳐"는 금지다. "src/auth/validate.ts 42번 줄의 null pointer를 고쳐, Session.expired가 true인데 토큰이 캐시에 남아있을 때 user 필드가 undefined다"처럼 구체적으로 지시해야 한다.
이건 사람이 주니어 개발자에게 일을 시키는 것과 같다. "버그 고쳐"가 아니라 "이 파일 이 줄에서 이 조건일 때 이렇게 되는 걸 이렇게 바꿔"라고 해야 제대로 된 결과가 나온다.
Fork Subagent — 자기 자신을 복제한다
에이전트 시스템에 fork 기능이 있다. isForkSubagentEnabled()로 게이트되어 있고, feature flag FORK_SUBAGENT로 제어된다.
일반 서브에이전트는 컨텍스트 없이 시작한다. 포크는 부모의 전체 대화 컨텍스트를 상속한다.
// Fork subagent feature: when enabled, insert the "When to fork" section
// (fork semantics, directive-style prompts) and swap in fork-aware examples.시스템 프롬프트에 언제 포크해야 하는지 가이드가 있다.
Fork yourself when the intermediate tool output isn't worth keeping
in your context. The criterion is qualitative — "will I need this
output again" — not task size.
포크가 효율적인 이유는 프롬프트 캐시를 공유하기 때문이다. 부모와 같은 시스템 프롬프트를 쓰니까 캐시가 그대로 먹힌다. 그래서 포크에는 model 파라미터를 설정하지 말라고 한다 — 다른 모델을 쓰면 부모의 캐시를 못 쓴다.
그리고 두 가지 핵심 규칙이 있다.
"Don't peek." 포크가 작업 중일 때 결과 파일을 Read로 읽지 마라. 포크의 도구 호출 노이즈가 부모 컨텍스트에 들어와서, 포크를 쓰는 의미가 없어진다.
"Don't race." 포크가 아직 안 끝났으면 결과를 추측하지 마라. 어떤 형태로든 — 산문, 요약, 구조화된 출력으로든 — 포크 결과를 조작하지 마라.
이건 멀티스레드 프로그래밍의 원칙과 같다. 다른 스레드의 작업이 끝나기 전에 그 결과를 읽으면 안 된다. 레이스 컨디션. 게임 서버에서 매일 보는 문제다.
워크트리 격리 — 에이전트가 Git 브랜치를 자동으로 만든다
에이전트에 isolation: "worktree" 옵션을 줄 수 있다. Git worktree를 만들어서 에이전트가 격리된 레포지토리 복사본에서 작업한다.
In a git repository: creates a new git worktree inside
`.claude/worktrees/` with a new branch based on HEAD
에이전트가 변경을 하면 워크트리와 브랜치가 유지되고, 변경이 없으면 자동으로 정리된다. 세션이 끝날 때 워크트리에 남아있으면 유지할지 삭제할지 물어본다.
이건 실전에서 강력하다. 메인 브랜치를 건드리지 않고 에이전트에게 실험적인 리팩토링을 시킬 수 있다. 결과가 마음에 들면 브랜치를 머지하고, 아니면 버린다.
Hooks — 도구 호출 전후에 커스텀 스크립트를 건다
Claude Code의 Hooks 시스템은 도구 이벤트에 셸 커맨드를 바인딩하는 기능이다. 시스템 프롬프트에 이렇게 설명되어 있다.
Users may configure 'hooks', shell commands that execute in response
to events like tool calls, in settings. Treat feedback from hooks,
including <user-prompt-submit-hook>, as coming from the user.
도구가 실행되기 전, 후, 또는 특정 이벤트(프롬프트 제출, 알림 등)에 셸 명령을 걸 수 있다. Hook이 실패하면 Claude는 그 결과를 사용자 피드백으로 취급하고 다른 접근을 시도한다.
이걸 활용하면 Claude Code가 파일을 수정할 때마다 자동으로 lint를 돌리거나, 커밋 전에 테스트를 실행하거나, 특정 패턴의 코드를 감지해서 경고를 보내는 파이프라인을 만들 수 있다.
세션 종료 시에도 Hook이 돌 수 있다. CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS 환경변수로 세션 종료 Hook의 타임아웃을 조절한다.
다음 편 — 소름 돋는 엔지니어링 디테일
3편에서는 가장 흥미로운 부분을 다룬다. 가챠 시스템으로 동작하는 가상 펫 "Buddy", 파일을 읽기만 해도 자동 업데이트되는 "Magic Docs", 자리를 비웠다 돌아오면 요약을 해주는 "Away Summary", Anthropic 직원이 외부 레포에 기여할 때 정체를 숨기는 "Undercover Mode", 그리고 Claude Code에 내장된 크론 스케줄러.
"에이전트 목록이 전체 캐시 생성 토큰의 10.2%를 차지했다. 그래서 아키텍처를 바꿨다."