더 많은 글은 radarlog.kr에서.
Claude Code 소스코드를 뜯으면서 가장 놀란 건 도구나 아키텍처가 아니었다. buddy/ 폴더를 열었을 때다.
가상 펫이 있다. 가챠 시스템으로 동작하는. 레어리티가 5단계이고, 종족이 18종이다.
Buddy — 코딩 에이전트에 가챠 가상 펫이 있다
src/buddy/ 디렉토리. 파일 5개, 79KB. feature flag BUDDY로 게이트되어 있어서 아직 공개된 기능은 아니다. 하지만 구현은 완성되어 있다.
userId를 시드로 해서 결정론적으로 캐릭터가 생성된다. 같은 유저는 항상 같은 캐릭터를 받는다. PRNG(Pseudo-Random Number Generator)로 Mulberry32를 쓴다.
function mulberry32(seed: number): () => number {
let a = seed >>> 0
return function () {
a |= 0
a = (a + 0x6d2b79f5) | 0
let t = Math.imul(a ^ (a >>> 15), 1 | a)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}게임 개발자로서 이 코드를 보면 바로 알 수 있다. 가벼운 시드 기반 랜덤이다. Mulberry32는 게임 업계에서 맵 생성이나 아이템 드롭 테이블에 흔히 쓰는 PRNG다. 여기서도 정확히 같은 용도로 쓰인다.
종족은 18종이다. duck, goose, blob, cat, dragon, octopus, owl, penguin, turtle, snail, ghost, axolotl, capybara, cactus, robot, rabbit, mushroom, chonk. 소스코드에서 종족 이름이 직접 문자열로 안 들어가고 String.fromCharCode로 인코딩되어 있다.
export const duck = c(0x64,0x75,0x63,0x6b) as 'duck'
export const capybara = c(0x63,0x61,0x70,0x79,0x62,0x61,0x72,0x61) as 'capybara'왜 이렇게 했을까. 주석에 힌트가 있다. "One species name collides with a model-codename canary in excluded-strings.txt." 내부 빌드 파이프라인에서 모델 코드네임이 번들에 들어가는 걸 감지하는 시스템이 있는데, 종족 이름 중 하나가 그 코드네임과 겹친다. 문자열을 직접 쓰면 빌드 체크에 걸리니까 charCode로 우회한 것이다.
레어리티 시스템은 가챠 게임 그대로다.
export const RARITY_WEIGHTS = {
common: 60,
uncommon: 25,
rare: 10,
epic: 4,
legendary: 1,
} as constcommon 60%, uncommon 25%, rare 10%, epic 4%, legendary 1%. shiny 확률은 1%다. 모바일 가챠 게임의 확률 테이블과 구조가 같다.
각 캐릭터에는 스탯이 있다. DEBUGGING, PATIENCE, CHAOS, WISDOM, SNARK 5종. 레어리티에 따라 스탯 하한선이 올라간다.
const RARITY_FLOOR: Record<Rarity, number> = {
common: 5,
uncommon: 15,
rare: 25,
epic: 35,
legendary: 50,
}peak stat과 dump stat이 있다. 하나는 높고 하나는 낮다. RPG 캐릭터 생성 그대로다.
눈 모양도 6종(·, ✦, ×, ◉, @, °), 모자도 8종(none, crown, tophat, propeller, halo, wizard, beanie, tinyduck). common은 모자가 없고, uncommon 이상부터 모자가 나온다. tinyduck 모자는 오리 위에 작은 오리가 앉아있는 거다.
캐릭터의 "bones"(외형)는 userId 해시에서 결정론적으로 만들어지고, "soul"(이름, 성격)은 모델이 생성한다. bones는 저장하지 않고 매번 다시 계산한다. soul만 config에 저장한다. 왜냐하면 bones를 저장하면 유저가 config 파일을 편집해서 legendary를 조작할 수 있기 때문이다.
// bones last so stale bones fields in old-format configs get overridden
return { ...stored, ...bones }Config에 bones 필드가 남아 있어도 새로 계산한 bones가 덮어쓴다. 치트 방지. 게임 개발자의 본능이다.
salt 값은 'friend-2026-401'이다. 2026년 4월 1일. 만우절에 출시할 계획이었던 걸까.
Magic Docs — 파일을 읽기만 해도 자동 업데이트된다
src/services/MagicDocs/ 디렉토리. 이건 진짜 마법 같다.
마크다운 파일 첫 줄에 # MAGIC DOC: [제목]을 쓰면, Claude Code가 그 파일을 자동으로 업데이트한다. 대화 중에 새로운 정보가 생기면, 포크된 서브에이전트가 백그라운드에서 문서를 갱신한다.
// Magic Doc header pattern: # MAGIC DOC: [title]
const MAGIC_DOC_HEADER_PATTERN = /^#\s*MAGIC\s+DOC:\s*(.+)$/imFileReadTool에 리스너가 등록되어 있어서, 파일을 읽을 때마다 Magic Doc 헤더가 있는지 감지한다. 헤더 다음 줄에 이탤릭으로 지시사항을 쓸 수 있다.
# MAGIC DOC: API Reference
_Focus on endpoint changes and breaking modifications_
## Current Endpoints
..._Focus on endpoint changes..._ 부분이 업데이트 지시사항이다. 이걸로 "이 문서를 어떤 관점에서 업데이트할지"를 제어한다.
업데이트는 postSamplingHook으로 트리거된다. 모델 응답이 생성된 후, 마지막 어시스턴트 턴에 도구 호출이 있었으면 문서 업데이트를 시도한다. sequential() 래퍼로 동시 업데이트를 막는다.
이걸 활용하면 프로젝트의 아키텍처 문서, API 레퍼런스, TODO 리스트 같은 것들이 코딩 작업 중에 자동으로 최신화된다.
Away Summary — 자리를 비웠다 돌아오면 요약해준다
src/services/awaySummary.ts. 사용자가 잠시 자리를 비웠다 돌아오면 "무엇을 하고 있었고, 다음 단계가 뭔지"를 1~3문장으로 요약해준다.
function buildAwaySummaryPrompt(memory: string | null): string {
return `${memoryBlock}The user stepped away and is coming back.
Write exactly 1-3 short sentences. Start by stating the high-level task
— what they are building or debugging, not implementation details.
Next: the concrete next step. Skip status reports and commit recaps.`
}최근 30개 메시지만 잘라서 작은 모델(getSmallFastModel())에게 요약을 시킨다. 전체 대화를 다 보내지 않는다. 비용 효율적이다.
세션 메모리가 있으면 같이 넣어서 더 넓은 맥락을 준다. "구현 디테일이 아니라 하이레벨 태스크를 먼저 말해라"라는 프롬프트가 정확하다. 돌아온 사람은 "어떤 함수를 수정했다"보다 "인증 모듈 버그를 고치고 있었다"가 훨씬 유용하다.
Undercover Mode — Anthropic 직원의 위장 모드
src/utils/undercover.ts. 이건 순수하게 Anthropic 내부용이다. USER_TYPE === 'ant'인 빌드에서만 동작한다.
Anthropic 직원이 오픈소스 레포에 기여할 때, 커밋 메시지나 PR 설명에서 내부 정보가 새나가지 않도록 하는 시스템이다.
export function isUndercover(): boolean {
if (process.env.USER_TYPE === 'ant') {
if (isEnvTruthy(process.env.CLAUDE_CODE_UNDERCOVER)) return true
return getRepoClassCached() !== 'internal'
}
return false
}기본값이 ON이다. 명시적으로 내부 레포라고 확인된 경우에만 꺼진다. 안전한 방향의 기본값.
위장 모드가 켜지면 시스템 프롬프트에 엄격한 규칙이 추가된다.
NEVER include in commit messages or PR descriptions:
- Internal model codenames (animal names like Capybara, Tengu, etc.)
- Unreleased model version numbers (e.g., opus-4-7, sonnet-4-8)
- Internal repo or project names
- The phrase "Claude Code" or any mention that you are an AI
- Co-Authored-By lines or any other attribution
모델 코드네임이 동물 이름이라는 게 여기서 확인된다. Capybara, Tengu 같은 이름들. "claude-opus-4-6"이 아니라 동물 이름 코드네임이 따로 있다.
Co-Authored-By 라인도 금지다. GitHub에서 AI 도구를 쓰면 보통 Co-Authored-By: Claude 같은 라인이 자동으로 붙는데, 위장 모드에서는 이걸 빼야 한다.
좋은 예시와 나쁜 예시가 프롬프트에 들어있다.
GOOD: "Fix race condition in file watcher initialization"
BAD: "Fix bug found while testing with Claude Capybara"
BAD: "1-shotted by claude-opus-4-6"
"1-shotted by claude-opus-4-6"이 나쁜 예시에 있다는 건, 실제로 이런 커밋 메시지를 쓴 사람이 있었다는 뜻이다.
크론 스케줄러 — 코딩 에이전트에 왜 크론이 있나
src/tools/ScheduleCronTool/. 3개의 하위 도구가 있다. CronCreateTool, CronDeleteTool, CronListTool.
Claude Code에 크론 스케줄러가 내장되어 있다. /loop 커맨드로 접근한다. 반복 작업을 스케줄링할 수 있다.
export function isKairosCronEnabled(): boolean {
return feature('AGENT_TRIGGERS')
? !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CRON) &&
getFeatureValue_CACHED_WITH_REFRESH('tengu_kairos_cron', true, ...)
: false
}기본값이 true다. 주석에 "/loop is GA (announced in changelog)"라고 되어 있다. 이미 출시된 기능이다. GrowthBook이 비활성화된 환경(Bedrock, Vertex 등)에서도 동작하도록 기본값을 true로 잡았다.
"Durable cron"이라는 옵션도 있다. 세션이 끝나도 디스크에 저장되어 유지되는 크론 태스크다. isDurableCronEnabled()로 별도 게이트된다. 세션-only 크론(메모리에만 있음)과 durable 크론(디스크에 저장)이 분리되어 있다.
CI/CD 없이도 "매시간 테스트 돌려", "매일 아침 의존성 업데이트 체크해" 같은 반복 작업을 Claude Code에 맡길 수 있다는 뜻이다.
세션 메모리 — 컨텍스트를 넘어서 기억한다
src/memdir/ 디렉토리. MEMORY.md 기반의 메모리 시스템이다.
MEMORY.md의 크기 제한이 소스코드에 명시되어 있다. 200줄 또는 25,000바이트. 이걸 넘으면 잘리고 경고가 붙는다.
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000잘릴 때 줄 수 초과인지 바이트 초과인지에 따라 다른 경고 메시지가 나온다. "인덱스 항목을 한 줄 200자 이내로 유지하고, 상세 내용은 토픽 파일로 분리하라"는 안내가 붙는다.
메모리 디렉토리에 쓸 때 mkdir을 먼저 하지 말라는 가이드가 시스템 프롬프트에 직접 들어가 있다.
export const DIR_EXISTS_GUIDANCE =
'This directory already exists — write to it directly with the Write tool
(do not run mkdir or check for its existence).'Claude가 mkdir부터 치느라 턴을 낭비하는 문제가 있었던 거다. 디렉토리를 미리 만들어놓고 "이미 있으니까 바로 써"라고 알려준다. API 호출 한 번이 곧 돈이다.
Auto Memory라는 기능도 있다. CLAUDE_CODE_DISABLE_AUTO_MEMORY=1로 끌 수 있는데, 기본은 켜져 있다. 대화 중에 중요한 패턴이나 선호도를 자동으로 메모리에 저장한다. bare 모드(CLAUDE_CODE_SIMPLE)와 remote 모드에서는 자동으로 비활성화된다.
팀 메모리(TEAMMEM feature)도 있다. teamMemorySync/ 디렉토리에 파일 변경 감시, 비밀 스캐닝(secretScanner.ts), 동기화 로직이 구현되어 있다. 팀원들이 공유하는 메모리인데, 비밀이 실수로 들어가지 않도록 스캐너가 돌아간다.
알림 시스템 — 터미널별로 다르다
src/services/notifier.ts. 사용자 설정에 따라 알림 채널이 바뀐다. iTerm2, Kitty, 터미널 벨, 시스템 알림 중에서 선택한다.
switch (channel) {
case 'iterm2':
terminal.notifyITerm2(opts)
return 'iterm2'
case 'kitty':
terminal.notifyKitty({ ...opts, title, id: generateKittyId() })
return 'kitty'iTerm2는 전용 escape sequence를 쓰고, Kitty는 Kitty 프로토콜을 쓴다. auto 모드에서는 터미널 종류를 감지해서 적절한 채널을 고른다.
Hook으로도 알림을 보낼 수 있다. executeNotificationHooks(notif)가 먼저 실행되고, 그 다음 터미널 알림이 간다. Slack이나 Discord 웹훅으로 알림을 보내는 Hook을 만들 수 있다는 뜻이다.
소스코드가 말해주는 것
1,884개 파일을 뜯어보면서 느낀 건, Claude Code가 단순한 "AI 코딩 도구"가 아니라 하나의 운영체제에 가까운 플랫폼이라는 거다.
프로세스 관리(에이전트 시스템), 스케줄링(크론), 파일시스템(메모리, 워크트리), 입력 처리(키바인딩, 마우스, 음성), 알림 시스템, 플러그인 아키텍처, 퍼미션 시스템. OS가 제공하는 기능의 대부분을 자체 구현하고 있다.
UE5를 쓰면서도 비슷한 느낌을 받는다. "이건 게임 엔진이 아니라 사실상 OS다"라는. Claude Code도 같다. 터미널 안에서 돌아가는 AI 운영체제.
그리고 곳곳에 묻혀 있는 주석들 — "10.2% of fleet cache_creation tokens", "research shows ~1.2% output token reduction" — 이 숫자들이 Anthropic이 제품을 어떤 수준으로 최적화하는지 보여준다. 감이 아니라 데이터로 프롬프트를 튜닝한다. 1.2%를 줄이기 위해 프롬프트 문구를 바꾸고, 10.2%를 줄이기 위해 아키텍처를 재설계한다.
코드를 직접 뜯어보지 않았으면 이런 건 절대 몰랐을 거다.
"문서에 환경변수 30개가 있으면, 소스코드에는 195개가 있다. 좋은 도구는 항상 보이는 것보다 깊다."