이번 시간에는 CLAUDE.md가 1,500줄 가까이 길어질 때 Claude Code가 그 안의 규칙을 덜 지키는지를 claude -p로 측정하며 배워가는 시간입니다. claude -p는 대화 화면 없이 질문 하나를 보내고 결과를 받는 실행 방식이에요. 무관한 팀 규칙 사이에 규칙 다섯 줄을 숨겨 두고, CLAUDE.md가 없을 때와 길이를 8줄부터 1,499줄까지 늘렸을 때의 준수율과 입력 토큰을 볼게요.
모델 출력은 비결정적이라 다시 돌리면 숫자가 달라질 수 있어요. --model opus가 가리키는 모델도 시간이 지나면 달라질 수 있습니다.
실습 준비
이 글의 명령은 모두 실습 코드의 claude-md-length-compliance/ 디렉터리에서 실행합니다.
run.sh: 조건 하나의 과제 20개를 내 설정이 섞이지 않게 격리한claude -p로 돌려out/<조건>/에 결과 JSON을 남기는 진입점conditions/:build.py가 만든 조건별 CLAUDE.md 디렉터리tasks/: 과제 20개의 질문 파일(tasks/prompts/t01.txt~t20.txt)과 과제 목록(파일 22개)grade.py: 결과 JSON을 심어 둔 규칙별로 채점해out/grades.jsonl에 쓰는 채점기filler/: 무관한 지침 생성(gen.sh)·거르기(filter.py) 스크립트, 주제 목록, 생성 원본과 결과filler.mdrules/: 심은 규칙 5개, 추가 규칙 10개, 비슷한 규칙 파일(파일 3개)build.py:filler/filler.md와rules/로 조건별 CLAUDE.md를conditions/에 쓰고build/manifest.txt에 줄 수·해시를 남기는 스크립트build/: 조건 목록manifest.txt와--append-system-prompt용sys_1500_dispersed.md(이 글에서는 쓰지 않아요)aggregate.py:out/grades.jsonl을 조건·규칙별 준수율과 조건 비교로 집계하는 스크립트exclude.txt: 규칙 없이도 지킨 비율이 높아aggregate.py가 결론 집계에서 빼는 규칙 목록
그 밖의 파일은 데이터·보조 스크립트입니다.
claude -p를 그냥 실행하면 사용자 홈의 ~/.claude/CLAUDE.md, 사용자 설정(권한 허용 목록, effort 기본값, 훅), 계정에 연결된 MCP 서버가 함께 들어가요. 길이 실험에서 이것들이 섞이면 토큰도 준수율도 내 환경의 값이 되겠죠.
그래서 run.sh는 모든 호출을 CLAUDE.md 하나만 둔 조건 디렉터리에서 다음 조합으로 돌려요. ./run.sh <조건> <반복 수>로 부르면 conditions/<조건>/을 작업 디렉터리로 tasks/prompts/의 과제 20개를 보내고, 결과 JSON을 out/<조건>/에 남깁니다.
--setting-sources project: 사용자 설정과 사용자 CLAUDE.md를 빼고, 작업 디렉터리의 프로젝트 CLAUDE.md만 읽게 합니다.--strict-mcp-config:--mcp-config로 준 MCP 서버만 씁니다. 여기서는 아무것도 주지 않았으므로 MCP 서버가 하나도 붙지 않아요.--tools "": 내장 도구를 모두 끕니다. 모델은 파일을 쓰지 못하고 답을 텍스트로만 내요.--effort medium: 지정하지 않으면 부모 셸의CLAUDE_EFFORT환경 변수나 사용자 설정의effortLevel이 끼어들 수 있어 명시했습니다.--max-turns 1:claude --help에는 없지만 동작합니다. 도구가 없으니 한 번에 끝나야 하고, 이를 안전장치로 걸었어요.--no-session-persistence: 대화 기록 파일을 남기지 않습니다.
--safe-mode는 쓰지 않았어요. claude --help의 설명대로 safe 모드는 CLAUDE.md까지 끄기 때문입니다. 한도 오류가 나오면 그 조건을 멈추고, 같은 명령으로 다시 돌리면 남은 과제만 이어서 돕니다.
Python 스크립트는 호스트에 아무것도 설치하지 않도록 python:3.12-slim 컨테이너에서 돌려요. 아래 블록의 py 함수가 그 일을 합니다.
아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.
py() { docker run --rm --network none -v "$PWD":/w -w /w python:3.12-slim python "$@"; }
심은 규칙은 다음 다섯 줄이에요. 강조어 없이 평서문으로 썼습니다.
- R1: 응답 첫 줄은
요약:으로 시작하는 한 문장으로 쓴다. - R2: 새 파이썬 파일의 코드를 보여 줄 때는 코드 블록 첫 줄에
# file: <경로>주석을 쓰고, 파일 이름은hp_로 시작하는 snake_case로 짓는다(예:# file: src/hp_duration.py). - R3: 변수·인자·함수·속성 이름에
result,data,tmp라는 단어를 쓰지 않는다.parsed,payload처럼 뜻이 드러나는 이름을 쓴다. - R4: 예외를 일으킬 때는 내장 예외(
ValueError,TypeError등) 대신from hp.errors import HpError로 가져온HpError를 쓴다. - R5: 응답 마지막 줄은
검증:으로 시작하고 그 뒤에 이 코드를 확인할 pytest 명령 한 줄을 적는다(예:검증: pytest -q tests/test_hp_duration.py).
고른 기준은 세 가지예요. 모델이 규칙 없이도 원래 하는 행동이 아니어야 하고, 사람이 보지 않고 정규식이나 Python AST(코드를 구문 단위로 나눈 트리)로 판정할 수 있어야 하며, Claude Code가 스스로 넣는 커밋·PR 지침과 겹치지 않아야 합니다.
과제는 Python 함수 하나를 만드는 한국어 요청 20개예요. 기간 문자열 파싱, slugify, 휴대폰 번호 검증, semver 비교, 로마 숫자, Luhn 검사, 구간 병합, IPv4 검증처럼 짧은 함수이고, 문장 형식은 모두 같습니다.
"1h30m", "45s" 같은 기간 문자열을 초 단위 정수로 바꾸는 함수 `parse_duration`을 새 파이썬 파일 하나에 들어갈 코드로 작성해서 답변에 보여 주세요. 입력이 잘못되면 예외를 일으켜야 합니다.
도구를 끈 상태에서 "새 파일로 작성해 주세요"라고 물으면 모델이 코드 블록 없이 "gcd.py에 작성했습니다"라고 답한 사례가 있어서, "답변에 보여 주세요"로 바꿨어요. 채점기 grade.py가 보는 것은 다음과 같아요.
- R1: 비어 있지 않은 첫 줄이
요약:으로 시작하면 지킴 - R2:
ast.parse가 성공하는 첫 코드 블록의 첫 줄이# file: (경로/)hp_이름.py형식이면 지킴 - R3: 그 코드 블록에서 모델이 지은 이름을
_로 나눈 조각에result,data,tmp가 있으면 어김 - R4:
raise문의 예외 이름이 모두HpError면 지킴. 인자 없는raise는 세지 않고, 센 것이 없으면 판정하지 않습니다. - R5: 비어 있지 않은 마지막 줄이
검증: pytest로 시작하면 지킴
규칙을 숨길 무관한 지침은 실제 팀 CLAUDE.md처럼 ## 제목 아래 글머리표가 이어지는 문서여야 해요. 1,500줄을 사람이 쓸 수는 없어서 Claude로 생성했습니다. Kotlin·Spring 서비스, React, Terraform, Kubernetes 배포, SQL 마이그레이션, Kafka처럼 Python과 응답 형식이 아닌 주제마다, filler/gen.sh <번호>가 --safe-mode로 한 번씩 "팀 저장소의 개발 규칙을 150줄 안팎으로 써 달라"고 요청하고 결과 JSON을 filler/raw/에 남깁니다.
그다음 filler/filter.py가 python, pytest, 요약, 검증, 예외, result, data, IMPORTANT, 응답처럼 심은 규칙과 겹치거나 경쟁할 낱말이 든 줄을 지우고, 주제 순서대로 이어 붙여 1,550줄을 넘으면 멈추고 filler/filler.md로 씁니다.
py filler/filter.py | tail -1
topics used: 14 generated 1674 dropped 214 (13%) filler.md 1651 lines, 191 sections
길이 조건은 이 파일의 앞부분을 섹션 단위로 잘라 써요. 기본 배치(흩어 둠)에서는 규칙 i를 파일 길이의 i/6 지점에 가장 가까운 섹션 끝에 한 줄로 끼우고, 따로 제목을 달지 않았어요. 출력은 이 글에서 쓰는 조건만 남긴 발췌예요.
py build.py | grep -v pre-
998 lines 89281 bytes 200e7c2ef46ae06b conditions/disp-1000/CLAUDE.md
1499 lines 134496 bytes 861a48c94389c34c conditions/disp-1500/CLAUDE.md
199 lines 17467 bytes bed7e65c95663888 conditions/disp-200/CLAUDE.md
496 lines 44516 bytes 703392c89f99a7e3 conditions/disp-500/CLAUDE.md
8 lines 821 bytes b8f999a30b2ee297 conditions/rules-only/CLAUDE.md
섹션 경계에서 자르기 때문에 목표 줄 수(200, 500, 1,000, 1,500)와 몇 줄씩 달라요. 그래서 이후에는 실제 줄 수로 적습니다. rules-only는 규칙 다섯 줄만 둔 8줄 CLAUDE.md예요.
run.sh는 시작할 때 조건 디렉터리부터 /까지 올라가며 다른 CLAUDE.md, CLAUDE.local.md, AGENTS.md, .claude/가 없는지 확인해요. Claude Code는 작업 디렉터리 위의 모든 상위 디렉터리에서 이 파일들을 읽기 때문입니다.
집계는 aggregate.py가 out/grades.jsonl을 읽어 out/summary.txt로 내요. 인자로 조건 이름을 주면 그 조건만, --rules를 주면 그 규칙 줄만 보여 줍니다. 이후 블록의 출력에는 run.sh와 grade.py가 내는 진행 줄(<조건> r1: 20개 실행 (시간), graded <N> calls -> out/grades.jsonl 같은 줄)을 옮기지 않았어요.
실습 1: 규칙이 없을 때
먼저 CLAUDE.md 없는 빈 디렉터리에서 같은 과제 20개를 3회 돌려 볼게요. 규칙이 없으니, 여기서 지킨 비율은 모델이 원래 그렇게 하는 비율이에요.
./run.sh none 3
py grade.py
py aggregate.py none --rules R1,R2,R3,R4,R5 \
| grep -vE "^models|^excluded|^$|^ all|cost|duration|cache|r2_header"
== none lines 0 bytes 0 calls 60 complete 60 reps [1, 2, 3]
not_success 0 pseudo_tool 0 no_code 3 parse_error 3 func_defined 57/60 num_turns!=1 0
R1 strict 0/60 0.0% [r1 0/20, r2 0/20, r3 0/20] lenient 0/60 0.0% clean 0/57
R2 strict 0/60 0.0% [r1 0/20, r2 0/20, r3 0/20] lenient 0/60 0.0% clean 0/57
R3 strict 51/60 85.0% [r1 17/20, r2 17/20, r3 17/20] lenient 51/60 85.0% clean 48/57 (excluded)
R4 strict 0/60 0.0% [r1 0/20, r2 0/20, r3 0/20] lenient 0/60 0.0% clean 0/57
R5 strict 0/60 0.0% [r1 0/20, r2 0/20, r3 0/20] lenient 0/60 0.0% clean 0/57
input_tokens median 2812 min 2799 max 2828
R3 banned names {'result': 9, 'data': 3}
출력을 보면 R1, R2, R4, R5는 60번 모두 0이었어요. 그러니 이 네 규칙을 지켰다면 그건 CLAUDE.md 덕분이라고 봐도 됩니다.
R3는 규칙 없이도 85%를 지켰어요. 모델이 원래 result, data, tmp 같은 이름을 잘 쓰지 않는다는 뜻이라, R3는 준수율이 높아도 규칙 덕이라고 말할 수 없어서 결론에서 뺐어요(exclude.txt). 이후 출력의 all-concl은 결론에 쓰는 규칙 넷(R1, R2, R4, R5)을 한 호출에서 모두 지킨 비율이에요. CLAUDE.md가 없을 때 호출 하나의 입력은 2,812토큰(중앙값)이었어요.
실습 2: 길이를 늘리면
규칙 다섯 줄을 흩어 둔 1,499줄 조건(disp-1500)을 가장 먼저 1회 돌렸어요.
실험 전에 정해 둔 규칙이 있었습니다. 여기서 결론 규칙이 모두 20번 중 19번 이상 지켜지면 더 오를 여지가 없는 천장 상태예요. 그때는 나머지 길이를 1회만 돌려 토큰 측정과 참고용 준수율로 쓰기로 했어요. 1,499줄은 20번 모두 지켰습니다. 그래서 1,499줄만 3회, 나머지는 1회 돌렸어요.
./run.sh disp-1500 3
./run.sh rules-only 1
./run.sh disp-200 1
./run.sh disp-500 1
./run.sh disp-1000 1
py grade.py
py aggregate.py rules-only disp-200 disp-500 disp-1000 disp-1500 \
| grep -E "^==|all-concl|input_tokens|instructions|cost"
== rules-only lines 8 bytes 821 calls 20 complete 20 reps [1]
all-concl 20/20 100.0% [r1 20/20] (R1+R2+R4+R5)
input_tokens median 3360 min 3346 max 3375
instructions tokens (median of cond - none per task) 547 per line 68.4 per KB 666.3
cost estimate (API list price) r1 $0.67 r2+ $0.00 total $0.67
== disp-200 lines 199 bytes 17467 calls 20 complete 20 reps [1]
all-concl 20/20 100.0% [r1 20/20] (R1+R2+R4+R5)
input_tokens median 11242 min 11229 max 11258
instructions tokens (median of cond - none per task) 8430 per line 42.4 per KB 482.6
cost estimate (API list price) r1 $1.97 r2+ $0.00 total $1.97
== disp-500 lines 496 bytes 44516 calls 20 complete 20 reps [1]
all-concl 20/20 100.0% [r1 20/20] (R1+R2+R4+R5)
input_tokens median 24016 min 24003 max 24032
instructions tokens (median of cond - none per task) 21204 per line 42.8 per KB 476.3
cost estimate (API list price) r1 $4.05 r2+ $0.00 total $4.05
== disp-1000 lines 998 bytes 89281 calls 20 complete 20 reps [1]
all-concl 20/20 100.0% [r1 20/20] (R1+R2+R4+R5)
input_tokens median 45342 min 45328 max 45357
instructions tokens (median of cond - none per task) 42529 per line 42.6 per KB 476.3
cost estimate (API list price) r1 $7.47 r2+ $0.00 total $7.47
== disp-1500 lines 1499 bytes 134496 calls 60 complete 60 reps [1, 2, 3]
all-concl 60/60 100.0% [r1 20/20, r2 20/20, r3 20/20] (R1+R2+R4+R5)
input_tokens median 66552 min 66538 max 66567
instructions tokens (median of cond - none per task) 63739 per line 42.5 per KB 473.9
cost estimate (API list price) r1 $9.86 r2+ $1.65 total $11.51
instructions tokens는 같은 과제의 입력 토큰에서 CLAUDE.md 없는 조건의 값을 뺀 중앙값, 곧 지침 블록이 차지한 토큰이에요. 출력을 보면 토큰과 준수율은 다르게 움직였습니다.
- 토큰: 지침 블록은 줄당 약 42.5토큰, 1KB당 약 474토큰으로 줄 수에 비례해 늘었어요. 199줄이 8,430토큰, 1,499줄이 63,739토큰입니다. 이 무관한 지침은 한 줄이 평균 90바이트쯤 되는 한국어 글머리표라서 줄당 토큰이 꽤 커요. 자기 파일의 값은 따로 재 보셔야 합니다.
- 준수율: 결론 규칙 넷을 어긴 호출이 어느 길이에서도 없었어요. 8줄도 1,499줄도 모두 지켰고, Memory 문서가 기준으로 말한 200줄 부근에서 꺾이는 모습도 없었습니다.
1,499줄 약 134KB 파일은 잘리지 않고 통째로 들어갔어요. Memory 문서도 "Claude Code loads a CLAUDE.md file of up to 4 MiB in full and skips a larger file."이라고 적습니다.
비용 추정치에는 캐시가 크게 작용했어요. 1,499줄 조건의 1회차 20호출은 $9.86(호출당 약 $0.49)이었는데, 2·3회차 40호출은 합쳐서 $1.65였습니다. 이 글에 싣지 않은 사전 확인에서 캐시 필드를 보면, 질문이 다르면 새 세션마다 CLAUDE.md까지 다시 캐시에 썼고 질문까지 같으면 캐시에서 읽었어요.
다만 비교하는 두 조건이 모두 95% 이상이면 차이를 볼 여지가 없으니, 길이 비교는 모두 "판정 불가(천장)"입니다. 차이를 찾지 못한 것을 길어도 괜찮다는 확인으로 쓰지는 않아요.
정리
범위: Claude Code 2.1.295의 claude -p, Opus 5.5(claude-opus-5-5), effort medium, 도구를 끈 한 턴 응답, Python 과제 20개, 조건마다 1회 또는 3회.
- 지침 블록은 줄당 약 42.5토큰으로 줄 수에 비례해 늘었고, 1,499줄이면 요청마다 63,739토큰이 더 실렸습니다. 자기 CLAUDE.md의 줄당 토큰은 따로 재 보세요.
- 규칙 없이 0%였던 네 규칙(R1, R2, R4, R5)이 8줄부터 1,499줄까지 모든 호출에서 지켜졌고, 200줄 부근에서 꺾이지도 않았어요. 1,499줄 외 길이는 1회만 돌렸고 모든 비교가 천장이었으니, 이 결과를 길어도 괜찮다는 증거로 쓰지는 마세요.
- 1,499줄 약 134KB 파일은 잘리지 않고 통째로 들어갔어요. 비용 추정치는 1회차 20호출이 $9.86이었고, 질문까지 같은 2·3회차 40호출은 합쳐서 $1.65로 캐시가 크게 작용했습니다.