TL;DR와 결론
Claude Code에 계속 치게 되는 작업 네 개는 각각 다른 파일에 속합니다. 릴리스 체크리스트는 skill입니다. 절대 깨지면 안 되는 규칙은 PreToolUse hook입니다. 파일을 많이 읽는 감사는 subagent입니다. Claude가 볼 수 없는 데이터는 MCP 서버입니다.
우리는 테스트 34개짜리 청구서 API라는 실제 레포 하나에서 네 가지를 모두 돌렸고, 매번 틀린 선택을 먼저 했습니다. 틀린 선택은 화면에서 실패했습니다.
- CLAUDE.md에 넣은 체크리스트는 /context에
Memory files: 210 tokens줄을 더했고, 모든 턴마다 로드됐습니다. - 첫 hook은 실행되어 경고를 출력했는데도 쓰기가 통과했습니다(unitPrice 8000에서 12000).
- 인라인으로 돌린 감사는 스레드를 가득 채웠습니다. Messages가 19.3k 토큰이었고, 이름을 부른 subagent에서는 7.2k였습니다.
- docs 서버가 없을 때 Claude는 "I couldn't find that setting... I won't guess one."이라고 답했습니다.
결론: docs의 표에서 종류를 고른 다음, 파일이 그저 존재하는 데서 끝나지 말고 답하게 만드세요. 기록만 하는 hook은 그것이 대체한 CLAUDE.md의 한 줄처럼 권고일 뿐입니다. 네 가지 중 묻지 않는 것은 hook뿐입니다.
아래 파일을 복사해 커밋하고, 가장 자주 다시 치는 것부터 시작하세요.
출처를 단 종합
첫 마디를 하기 전에 세션이 불러오는 것. 우리 기준선 /context는 26.2k/1m 토큰이었고, 메모리 줄도 MCP 줄도 없었습니다. 작업을 하나 더할 때마다 그 표에 줄이 생기거나 생기지 않습니다. 먼저 읽고, 그다음 그 기준선과 비교해 측정하세요.
CLAUDE.md의 규칙은 부탁이고, hook은 강제입니다. docs는 CLAUDE.md나 skill에 적은 "never edit .env" 같은 지시는 부탁이고, 편집을 막는 PreToolUse hook은 강제라고 말합니다 s1. Memory 페이지는 CLAUDE.md를 강제가 아니라 컨텍스트라고 부릅니다 s8. Hacker News의 한 스레드도 hook 쪽에서 같은 것을 보고했습니다. 0으로 종료하는 hook은 아무것도 막지 않습니다 s10.
트리거 표가 어느 파일인지 알려 줍니다. 같은 절차를 세 번째 붙여 넣는다면 skill입니다. 출력이 채팅을 넘치게 하면 subagent입니다. Claude가 볼 수 없는 탭이라면 MCP 서버입니다. 매번 반드시 일어나야 하는 일이면 hook입니다 s1.
작업 1, 릴리스 체크리스트는 skill입니다.
- CLAUDE.md 안에서 커진 절차는 skill에 속하며, skill의 본문은 쓸 때만 로드됩니다 s3.
- 우리 실행에서는 CLAUDE.md에 붙여 넣은 체크리스트가
Memory files: 210 tokens줄을 더했습니다.disable-model-invocation: true를 붙인 skill로 만들자 /context는 기준선으로 돌아갔습니다. /release는 여섯 단계를 실행했습니다(테스트 34개, 0.4.0에서 0.4.1, 태그 v0.4.1). 또git commit -am이 추적되지 않는 파일을 건너뛴다는 점도 잡아내서, 새 changelog를 먼저 stage했습니다.
작업 2, 지켜야 할 규칙은 hook이고, 보장은 hook이 돌려주는 응답입니다.
- 출력 없이 0으로 종료하면 결정이 아닙니다. 2로 종료하면 막힙니다. deny JSON도 막습니다 s2.
- 우리의 첫 hook은 기록하고 0으로 종료했고 쓰기가 통과했습니다(unitPrice 8000에서 12000). Claude의 답변에는 hook이 한 번도 언급되지 않았습니다.
- 같은 hook이 deny JSON을 돌려주자 편집이 거부되었고 다음 오류가 나왔습니다.
PreToolUse:Edit hook error: Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions).그러자 Claude는 무시할지, 마이그레이션을 쓸지 물었습니다. - docs는 보호 대상 파일을 위한 PreToolUse 레시피를 공개하고 있습니다 s23.
- 한계: 이 hook은 Edit와 Write만 감시합니다. 셸도 덮거나, 셸을 허용 목록에서 빼 두세요.
onFailure: "block"은 실패한 hook이 열린 채로 실패하지 않고 막게 합니다. v2.1.295에서 추가되었습니다 s2 s15.
작업 3, 넘쳐나는 감사는 subagent입니다.
- subagent는 아무것도 모르는 상태로 시작해 요약을 돌려줍니다 s4.
- 인라인에서는 Messages가 19.3k였습니다. 이름을 부른 subagent에서는 7.2k였습니다.
- agent 파일만으로는 2번 중 0번 위임되었습니다. 프롬프트에서 이름을 부르면 위임되었습니다. 파일은 subagent를 존재하게 하고, 당신의 프롬프트는 Claude가 그것을 쓰게 합니다.
- 파일에서
model:을 고정하세요. 비용은 변하지 않았습니다. 읽기는 여전히 일어나고, 당신의 윈도우 안에서만 일어나지 않습니다.
작업 4, Claude가 닿을 수 없는 데이터는 MCP 서버입니다.
claude mcp add --scope project는.mcp.json을 쓰고, tool search는 도구 이름만 로드합니다 s5.- Claude Code 자체 docs를 위한, 무료이고 로그인이 필요 없는 HTTP 서버가 있으며
search_claude_code_docs와query_docs_filesystem_claude_code_docs를 제공합니다 s24. - 서버가 없을 때 Claude는 "I couldn't find that setting... I won't guess one."이라고 답했습니다. 서버가 있을 때는 우리 설치가 2.1.294였는데
"onFailure": "block", 즉 v2.1.295에서 추가된 설정이라고 답했습니다. - 모델이 이미 아는 라이브러리의 docs 서버는 우리에게 아무것도 바꾸지 못했습니다. 모델이 가질 수 없는 것을 연결하세요.
현장 보고. 한 Reddit 스레드는 Claude가 CLAUDE.md의 "MUST use agent" 줄을 무시한다고 보고했습니다 s9. 우리는 이것을 측정이 아니라 보고로 인용합니다. Anthropic의 엔지니어링 글도 같은 구분을 다룹니다 s6 s7.
코드
이번 실행의 파일 네 개와 보조 파일 두 개입니다. 경로는 각 파일이 당신의 레포에서 놓일 위치입니다.
1. 릴리스 skill, 위치는 .claude/skills/release/SKILL.md입니다. /release를 치세요. disable-model-invocation: true는 시작을 당신만 할 수 있게 하며, 커밋하고 태그를 다는 작업에는 바로 이것이 필요합니다.
SKILL.md
---
name: release
description: Cut a patch release of ledgerly (typecheck, tests, version bump, changelog line, commit, tag).
disable-model-invocation: true
---
Run these steps in order and stop at the first failure:
1. `pnpm typecheck` must exit 0.
2. `pnpm test` must pass.
3. Bump the patch version in `package.json` (e.g. 0.4.0 -> 0.4.1).
4. Add one line at the top of `CHANGELOG.md` (create it if missing): `## vX.Y.Z - YYYY-MM-DD` followed by a one-line summary of the commits since the last tag.
5. `git commit -am "release vX.Y.Z"`
6. `git tag vX.Y.Z`. Never push.
2. 데이터 보호 hook, 위치는 .claude/settings.json입니다. Claude가 읽을 이유와 함께 deny 결정을 돌려줍니다. jq가 PATH에 있어야 하고, 인라인으로 들어가므로 hook에는 자체 스크립트 파일이 필요 없습니다. data/.*\.json$ 패턴과 이유를 당신의 규칙에 맞게 바꾸세요.
settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -c 'if (.tool_input.file_path // \"\") | test(\"data/.*\\\\.json$\") then {hookSpecificOutput: {hookEventName: \"PreToolUse\", permissionDecision: \"deny\", permissionDecisionReason: \"Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions).\"}} else empty end'"
}
]
}
]
}
}
3. conventions auditor, 위치는 .claude/agents/conventions-auditor.md입니다. 도구는 읽기 전용으로 제한되어 있고 모델은 고정되어 있습니다. 규칙 목록은 당신의 README에 맞게 고치세요.
conventions-auditor.md
---
name: conventions-auditor
description: Use when asked to audit the code against the README conventions
model: sonnet
tools: Read, Grep, Glob
---
You audit ledgerly's source against README § Conventions:
- Money is always an integer number of cents. Never floats.
- Domain functions return new objects; they never mutate their input.
- Validation failures throw `ValidationError` (400), missing records `NotFoundError` (404), illegal state transitions `ConflictError` (409).
- A change of stored shape bumps `schemaVersion` and adds a migration in `src/store/migrations.ts`.
Read every file under `src/`. Return only the findings: one line per violation as `file:line - convention - what is wrong`, then borderline cases the same way, then one line with the count of files read. No file contents, no summary of clean files.
conventions-auditor.md on GitHub
4. MCP 설정, 레포 루트에 .mcp.json으로 저장합니다. claude mcp add --transport http claude-docs --scope project https://code.claude.com/docs/mcp가 쓰는 내용 그대로입니다. Claude Code는 project 서버를 다음 세션에서 승인하기 전까지 pending으로 표시합니다.
mcp.json
{
"mcpServers": {
"claude-docs": {
"type": "http",
"url": "https://code.claude.com/docs/mcp"
}
}
}
보조 파일. ctx.sh는 헤드리스 세션에서 /context를 실행해 토큰 줄을 출력하므로, 전후를 비교할 수 있습니다. mcp-probe.sh는 HTTP MCP url에 initialize와 tools/list를 보내서, 추가하기 전에 서버가 응답하는지 확인합니다.
ctx.sh
#!/usr/bin/env bash
# Usage: ctx.sh <step> -> runs "/context" through run-session.sh, writes context-<step>.txt (honours STRICT_MCP / EXTRA_ALLOW)
C="$(cd "$(dirname "$0")" && pwd)"
"$C/run-session.sh" "$C/out/context-$1.json" "/context" >/dev/null
python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print(d['result'])" "$C/out/context-$1.json" > "$C/context-$1.txt"
grep -E '^\*\*Tokens|^\| (System prompt|System tools|MCP tools|Memory files|Skills|Custom agents|Messages) ' "$C/context-$1.txt"
mcp-probe.sh
#!/usr/bin/env bash
# Usage: mcp-probe.sh <url> [tools/call json params] -> initialize + notifications/initialized + tools/list (+ optional tools/call) over streamable HTTP
U="$1"; H=$(mktemp)
curl -s -D "$H" -m 30 -X POST "$U" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
echo; SID=$(grep -i '^mcp-session-id:' "$H" | tr -d '\r' | cut -d' ' -f2); echo "session-id: ${SID:-none}"
HS=(-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream'); [ -n "$SID" ] && HS+=(-H "Mcp-Session-Id: $SID")
curl -s -m 30 -X POST "$U" "${HS[@]}" -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
curl -s -m 30 -X POST "$U" "${HS[@]}" -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; echo
[ -n "${2:-}" ] && { curl -s -m 90 -X POST "$U" "${HS[@]}" -d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":$2}"; echo; }
rm -f "$H"
월요일에 할 일
- 새 세션을 열고 /context를 실행하세요. 합계를 적어 두세요. 그것이 기준선입니다.
- 가장 자주 붙여 넣는 체크리스트를 고르세요.
.claude/skills/<name>/SKILL.md로 옮기고, 커밋하거나 태그를 다는 것이면disable-model-invocation: true를 추가한 뒤/<name>으로 실행하세요. - 절대 깨지면 안 되는 규칙 하나를
permissionDecision: "deny"를 돌려주거나 2로 종료하는 PreToolUse hook에 넣으세요. Claude에게 그 규칙을 깨 보라고 하고, 편집이 거부되는지 확인하세요. - hook이 기록만 한다면 바꾸세요. 0으로 종료하는 hook은 아무것도 막지 않습니다.
- 파일을 많이 읽는 작업 하나를
model:을 고정해.claude/agents/<name>.md로 옮기세요. Claude가 스스로 고를 때까지는 프롬프트에서 이름을 부르세요. - Claude가 최근에 추측으로 알려 준 사실 하나를 확인하세요. 그것이 모델보다 새로운 것이라면
claude mcp add --transport http <name> --scope project <url>로 docs 서버를 추가하고.mcp.json을 커밋하세요. - 설정에 의존하기 전에 Claude Code 버전을 확인하세요.
onFailure: "block"은 2.1.295가 필요합니다. -
git add -A && git status를 실행하고 새 파일을 커밋하세요. 우리 실행에서는 정확히 네 개가 나왔습니다.
더 알아보기
선택 카드. 한 페이지입니다. docs의 행 s1에 우리의 작업 네 개를 적어 넣은 것입니다. 이 팩의 핵심은 위의 파일 네 개와 이 카드입니다.
| 종류 | 하는 일 | 이럴 때 쓰기 | 우리 작업 | 우리 파일 |
|---|---|---|---|---|
| CLAUDE.md | 모든 대화에 로드되는 지속 컨텍스트 | Claude가 두 번 틀린 규칙 | 절차가 아니라 한 줄 | 없음 |
| Skill | Claude가 쓸 수 있는 지시와 워크플로 | 같은 플레이북을 세 번째 붙여 넣을 때 | 릴리스 체크리스트 | .claude/skills/release/SKILL.md |
| Hook | 이벤트로 실행되는 스크립트 | 묻지 않고 매번 일어나길 원할 때 | 저장 데이터를 손으로 편집하지 않기 | .claude/settings.json |
| Subagent | 요약된 결과를 돌려주는 격리된 컨텍스트 | 부수 작업이 대화를 넘치게 할 때 | conventions 감사 | .claude/agents/conventions-auditor.md |
| MCP | 외부 서비스에 연결 | Claude가 볼 수 없는 탭의 데이터를 계속 복사할 때 | Claude Code docs | .mcp.json |
| Plugin | 네 가지를 설치 가능한 하나로 묶기 | 두 번째 레포에도 같은 구성이 필요할 때 | 사용하지 않음 | 없음 |
카드는 종류를 정할 뿐 품질을 정하지는 않습니다. 기록만 하는 hook, 이름을 부르지 않은 subagent, 모델이 아는 docs용 서버는 모두 올바른 종류에 있었는데도 실패했습니다. 종류를 고른 다음, 파일이 답하게 만드세요.
이것들을 쓰지 말아야 할 때.
- Claude가 계속 틀리는 규칙은 절차가 아니라 한 줄로 CLAUDE.md에 둡니다.
- 기록을 보면서 앞뒤로 방향을 잡는 작업은 subagent가 아니라 메인 스레드에 둡니다.
- 모델이 이미 아는 라이브러리의 docs 서버는 컨텍스트만 쓰고 아무것도 바꾸지 못합니다.
pnpm test는 이미 가진 CLI입니다. MCP의 일이 아닙니다.
Plugin. plugin은 네 가지를 함께 배포하는 상자입니다. 다른 레포에도 같은 구성이 필요할 때 쓰세요.
Skill과 MCP. docs는 둘을 짝지어 놓았습니다. 서버가 연결을 제공하고, skill이 그것을 잘 쓰는 법을 Claude에게 가르칩니다.
context: fork를 쓰는 skill. skill은 포크된 subagent에서 실행할 수 있어서, 읽기가 당신의 윈도우 밖에 머뭅니다. 의존하기 전에 작업 하나로 시험해 보세요.
command 외의 hook 유형. 우리가 쓴 것은 command입니다. hooks 레퍼런스에는 http와 prompt hook을 포함한 다른 핸들러 유형이 나와 있습니다. 규칙은 같습니다. hook이 돌려주는 것이 결정하고, 말이 없는 hook은 아무것도 결정하지 않습니다.
MCP와 CLI. MCP 서버가 컨텍스트를 잡아먹는다는 논쟁이 이어지고 있습니다. CLI를 먼저 쓰자는 글 s11과 MCP 서버의 컨텍스트 비용을 다룬 글 s12이 있습니다. 우리의 규칙은 이렇습니다. CLI가 이미 그 일을 한다면 CLI를 쓰세요. CLI가 없는 것에 MCP를 쓰세요. tool search는 이제 이름만 로드하므로, 결정하기 전에 당신의 환경에서 /context로 측정해 보세요 s5.
skill은 짧게, 검증해서. 작업마다 짧은 skill 하나를 만들고, 실제 실행으로 확인하세요. SkillsBench 논문 s13과, 쓰기 전까지 skill의 비용이 작은 이유를 다룬 실무자의 관점 s14을 참고하세요.
FAQ
hook이 실행됐는데 왜 편집이 통과했나요? hook은 명령이 돌려주는 것으로만 결정합니다. 출력 없이 0으로 종료하면 결정이 아닙니다 s2. deny JSON을 돌려주거나 2로 종료하세요.
subagent가 있는데 왜 Claude는 직접 파일을 읽나요? 우리 실행에서는 agent 파일만으로는 2번 중 0번 위임되었습니다. 프롬프트에서 이름을 부르세요. 예를 들어 "use the conventions auditor subagent"입니다.
hook이 Claude가 할 수 있는 모든 것을 덮나요? 아니요. 우리 hook은 Edit와 Write에 일치합니다. 이후 실행에서 Claude는 셸로 변경하겠다고 제안했으므로, 셸도 덮거나 허용 목록에서 빼 두세요.
쓰는 모든 라이브러리에 MCP 서버를 추가해야 하나요? 아니요. 모델이 이미 아는 라이브러리라면 서버는 아무것도 바꾸지 못하고 컨텍스트만 씁니다. 모델보다 새롭거나 디스크 밖에 있는 데이터에 추가하세요. 그리고 설치된 버전을 그 기능이 추가된 버전과 대조해 확인하세요.
이 모든 것을 커밋해도 되나요?
네. skill, settings 파일, agent 파일, .mcp.json이 모두 레포 안에 있으므로 클론하는 누구든 갖게 됩니다. project MCP 서버는 다음 세션에서 한 번 승인하세요.
AIDive