Claude가 "왜 아직도 안 고쳐졌나요?"라고 감지할 때 CLAUDE.md를 스스로 수리하는 법: 트랜스크립트의 좌절 신호로 구동되는 자기
요약
Claude Code 환경의 성능 저하를 감지하고 스스로 수리하는 자동화 스크립트 구현 방법을 소개합니다. 트랜스크립트 분석과 5가지 지표를 통해 CLAUDE.md 비대화나 불필요한 에이전트 주입을 감지하고, 문제가 발생하면 Claude가 직접 환경을 복구하도록 설계되었습니다.
핵심 포인트
- Claude Code 환경의 성능 저하를 감지하는 자동화 메커니즘 구축
- 5가지 정적·동적 지표와 임계값을 활용한 상태 모니터링
- 문제가 감지될 경우 Claude가 스스로 설정을 수정하는 self-repair 프로세스
- 수정 후 독립적 재측정을 통해 복구 성공 여부를 검증하는 신뢰성 확보
이전 포스트에서 저는 대화 로그로부터 스킬 사용 통계를 추출했습니다. 이번에는 반대로 진행합니다. 매주 루프를 돌며 트랜스크립트 (transcripts)를 읽어서 저의 Claude Code 환경이 저하되고 있는지 감지하고, 임계값 (threshold)을 넘어서면 claude -p가 스스로 문제를 해결하도록 합니다.
~/.claude/scripts/cc-self-audit.sh (177줄)는 매주 일요일 8:30에 launchd를 통해 실행됩니다. 이 스크립트는 5개의 수치를 수집하여 임계값과 비교하며, 모든 상태가 정상(green)이면 한 줄의 ✅ 메시지를 보냅니다. 만약 하나라도 비정상(red)이면, 스스로 수리(self-repair)를 수행하고, 독립적으로 재측정(re-measures)한 뒤 Discord에 보고합니다. 구현 노력의 약 70%는 "왜 단순한 grep 방식이 여기서는 작동하지 않는가"를 해결하는 데 투입되었습니다.
문제점: 아무도 환경 저하를 감지하지 못하고 있었다
2026-07-11에 수행한 성능 감사 (performance audit) 결과, 세 가지 문제가 동시에 발견되었습니다. 제거되었어야 할 에이전트(agents) 99개가 여전히 주입되고 있었고, CLAUDE.md 파일은 조용히 228KB까지 비대해졌으며, Stop hook이 매주 수십 번씩 발생하고 있었습니다.
이 각각의 문제들은 개별적으로 해결 가능합니다. 진짜 문제는 "고쳤지만 다시 망가지는" 사이클을 인지할 메커니즘이 없었다는 점입니다. 사람이 매주 육안으로 확인하는 방식은 지속 가능하지 않기에, 스크립트가 이를 수행하도록 만들었습니다.
전체 설계: 5가지 지표 × 임계값 × 독립적 재측정
launchd(일요일 8:30)
→ cc-self-audit.sh
① collect() → 5개 수치(정적 2개 + 동적 3개)
...
(흐름: 일요일 8:30 launchd 실행 → ① collect()가 5개의 수치(정적 2개 + 동적 3개)를 수집 → ② breaches()가 임계값 위반 사항을 나열 → ③ 정상(green)일 경우: ✅ 알림을 보내고 종료 → ④ 비정상(red)일 경우: claude -p (MAX 플랜의 Sonnet 모델)가 ~/.claude 내부의 문제들을 수정 → ⑤ 수치가 개선되었는지 확인하기 위해 collect()를 독립적으로 재실행 (자기 보고는 신뢰하지 않음) → ⑥ Discord #01_alerts 채널로 보고.)
임계값 (환경 변수를 통해 재정의 가능):
| 지표 (Metric) | 임계값 (Threshold) | 변수 (Variable) |
|---|---|---|
| inject_bytes | 40,000 B | SELF_AUDIT_TH_INJECT |
| ... |
정적 지표 (Static metrics): 주입된 바이트 및 에이전트 수
inject_bytes=$(( \
$(find "$HOME/.claude/rules" -name '*.md' -print0 2>/dev/null | xargs -0 cat 2>/dev/null | wc -c) + \
$(cat "$HOME/.claude/CLAUDE.md" 2>/dev/null | wc -c) + \
...
find ~/.claude/agents -name '*.md' 명령은 의도적으로 재귀적(recursive)으로 동작합니다. 이는 제가 이미 겪었던 실패 사례, 즉 점(dot)으로 시작하는 하위 디렉터리(예: .backup/)로 "아카이브"된 에이전트들이 다시 로드되는 문제를 재감지하기 위함입니다. 만약 agents_loaded가 60을 초과하면, 저는 이를 "아카이브 누수(archiving has leaked)"로 간주합니다.
동적 지표 (Dynamic metrics): 트랜스크립트 JSONL에서 사용자가 직접 입력한 메시지만 읽기
이것이 설계의 핵심입니다. "마지막 실행 이후 업데이트된 100KB 이상의 JSONL 파일 최대 200개"를 대상으로 하여, stopspam(감사 훅의 실제 실행 횟수)과 좌절(frustration, 사용자의 좌절 표현)을 계산합니다.
STOP_MARKER = 'Stop hook feedback:\n[~/.claude/hooks/self_audit_stop.sh]: '
FRUST_RE = re.compile(r'何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い')
...
(정규 표현식(regex)은 "몇 번이나 말해야 하나", "적당히 좀 해라", "거짓말쟁이", "왜 아직 안 고쳐지나" 등과 같은 일본어 구절을 매칭합니다. # ← ここが肝 주석은 결정적인 라인을 표시합니다.)
isMeta가 아닌 type=user 블록만 검사합니다. 즉, 실제로 사람이 직접 타이핑한 메시지만 확인합니다. 이것이 왜 중요한지는 다음 섹션에서 다룹니다.
2026-07-12의 오탐(False-positive) 사건
첫 번째 구현 방식은 다음과 같았습니다.
# 구 버전: 단순한 grep
stopspam=$(echo "$files" | xargs grep -h -c 'self_audit_stop' 2>/dev/null | awk '{s+=$1} END{print s+0}')
frustration=$(echo "$files" | xargs grep -hE -c 'なんで治らん|いい加減' 2>/dev/null | awk '{s+=$1} END{print s+0}')
history.jsonl에는 당시 측정된 값들이 여전히 남아 있습니다.
{"ts":"2026-07-11 22:37:28","inject_bytes":21798,"agents_loaded":48,"stopspam":6,"frustration":4,"toolerr":2}
{"ts":"2026-07-12 08:30:06","inject_bytes":21966,"agents_loaded":48,"stopspam":40,"frustration":18,"toolerr":67}
{"ts":"2026-07-12 08:40:03","inject_bytes":21966,"agents_loaded":48,"stopspam":58,"frustration":32,"toolerr":68}
07-11 저녁은 정상(GREEN)이었습니다. 07-12 아침에는 stopspam이 6에서 40으로, frustration이 4에서 18로 급증하며 RED 상태가 되었습니다. 그 후 claude -p가 수정을 시도하고 독립적인 재측정(re-measurement)이 실행되었으나, 수치는 58/32로 더욱 상승했습니다.
원인은 다음과 같습니다: grep이 **전체 JSONL에 대해 문자열 검색(string search)**을 수행하고 있었습니다. 트랜스크립트(transcripts)에는 다음 내용들이 포함되어 있었습니다:
- 수정 작업 자체에서 발생한 Diff (hook 스크립트의 소스 코드가
old_string/new_string에 통째로 들어감) - Read 도구로 열린 파일의 내용 (우연히 좌절(frustration) 관련 단어를 포함하고 있던 파일들)
- 되돌아온 tool_results (이전 감사(audit)의 출력이 다음 세션에서 인용됨)
이 모든 것들이 매칭되었습니다. 40번의 stopspam 탐지 중 실제 hook이 실행된 경우는 거의 0에 가까웠고, 18번의 frustration 탐지 중에서도 사람이 실제로 입력한 좌절 단어는 손에 꼽을 정도였습니다.
트랜스크립트를 대상으로 단순한 문자열 grep을 사용할 수는 없습니다. JSONL 내에서 "어떤 역할(role), 어떤 블록(block)"인지를 먼저 결정하지 않으면, 인용문, 도구 출력, 그리고 과거 로그의 에코(echoes)가 모두 카운트됩니다. 측정 설계의 해상도(resolution)가 감사(audit)의 품질을 결정합니다.
해결책은 JSONL 파서(parser)로 전환하는 것이었습니다. type=user이면서 isMeta: false인 텍스트 블록(사용자가 실제로 보낸 메시지)만 검사하며, stopspam 카운트는 하네스(harness)가 실제로 주입하는 정확한 고정 형식과 일치하는 라인만 계산하도록 했습니다. 이를 통해 인용문, 도구 출력, 과거 로그의 에코를 완전히 배제할 수 있었습니다.
임계값 초과(Threshold breach) → 자기 수리(self-repair) → 독립적 재측정(independent re-measurement)
상태가 RED로 변했을 때 claude -p에 전달되는 프롬프트의 핵심은 다음과 같습니다:
PROMPT="당신은 Claude Code 환경의 자기 감사·자기 수리 에이전트입니다.
## 감지된 위반 사항
...
(영어 프롬프트 내용: "당신은 Claude Code 환경을 위한 자기 감사(self-audit) / 자기 수리(self-repair) 에이전트입니다. ## 감지된 위반 사항: $BREACH. ## 지침: 1. ~/.claude 내부에서만 조사 및 수정하십시오 (프로젝트 코드, 비밀 정보, plist 삭제는 금지됩니다). 2. 각 변경 사항에 대해 날짜/대상/이유/되돌리는 방법(how-to-revert)을 ${CHANGELOG}에 추가하십시오. 3. 수정한 후에는 항상 bash ~/.claude/scripts/cc-self-audit.sh --collect-only를 실행하고 수치적으로 개선되었는지 확인하십시오. 4. 스스로 안전하게 수정할 수 없는 항목을 강제로 처리하지 마십시오. 'Needs human: 이유.'라는 한 줄 문구로 종료하십시오. 5. 최종 출력은 최대 3줄의 요약입니다.")
중요한 부분은 지침 3번, 즉 그 지침 외부에서 일어나는 일입니다. claude -p가 자기 자신을 다시 측정하게 하는 것에 그치지 않고, 하네스(harness)가 독립적으로 collect()를 실행하여 수치를 확인합니다 (165행 참조):
# 독립 재측정 (자기 보고를 믿지 않음)
AFTER=$(collect)
log "after: $AFTER"
...
만약 AI가 "수정했습니다"라고 말했지만 수치가 개선되지 않았다면, 🚨 경고가 발생합니다. Claude가 구축한 도구를 사용하여 Claude 자신의 환경을 수리할 때, 자기 보고(self-report)만 신뢰하는 것은 스스로를 해치는 지름길입니다.
기록된 수치가 보여주는 것
현재 history.jsonl이 알려주는 정보는 다음과 같습니다:
- inject_bytes: 21,798–21,966 B (40,000 임계값의 절반 미만, 초록색(GREEN))
- agents_loaded: 48 (임계값 60 미만, 초록색(GREEN))
- 07-12의 빨간색(red)은 오탐(false positive)이었습니다 (grep 수정 이후 수치가 정상으로 돌아옴)
처음 두 항목은 07-11에 수동으로 실행한 --collect-only의 전/후 결과이며, 3~4번 항목은 07-12의 grep 오탐 및 그에 따른 재측정 결과입니다. 주간 작업으로서의 운영(Production operation)은 08-30부터 시작됩니다.
launchd 설정
<key>StartCalendarInterval</key><dict>
<key>Weekday</key><integer>0</integer> <!-- 日曜 (일요일) -->
<key>Hour</key><integer>8</integer>
...
(Weekday 0과 日曜 주석은 일요일을 의미합니다.)
RunAtLoad는 false입니다. 만약 작업이 등록 즉시 실행된다면, 첫 번째 측정은 launchd가 시작된 직후의 "비어 있는" 상태에서 발생하게 됩니다. 대신, 작업을 등록하기 전에 --collect-only를 수동으로 실행하여 history.jsonl에 한 줄을 삽입함으로써 첫 번째 기준점(baseline) 측정을 수행합니다.
내가 겪은 함정들 (Pitfalls)
- 전체 트랜스크립트(transcript)에 대한 단순 grep 오탐지 → JSONL 파서를 사용하고
type=user && !isMeta인 경우만 타겟팅해야 합니다. - stopspam을 위한
self_audit_stop매칭이 훅 스크립트(hook script) 자체의 인용구까지 건드림 → 하네스(harness)가 실제로 주입하는 수정 마커 문자열(정확한 형식 일치)만 카운트해야 합니다. - AI는 "수정됨"이라고 보고했지만 수치는 변하지 않음 →
claude -p출력과 독립적으로collect()를 다시 실행하여HISTORY에 추가한 다음, 그제서야 판단해야 합니다. - launchd의 최소한의 PATH 환경에서
claude를 찾을 수 없음 → plist의EnvironmentVariables에PATH를 명시적으로 선언해야 합니다 (/opt/homebrew/bin과~/.local/bin이 필요합니다). frustration수치가 0에 고정되어 있으면 "아무 일도 일어나지 않는 것"처럼 보일 수 있음 → "좌절(frustration) 0"은 좋은 신호이지만, 임계값(threshold)과 정규 표현식(regex)이 여전히 실제 단어 집합을 잘 추적하고 있는지 매달 확인해야 합니다.
요약
- Claude Code 환경의 저하(에이전트 과부하, 인젝션 비대화, 훅 스팸, 사용자 좌절)는 5가지 지표를 통해 매주 측정되며, 임계값을 넘어서면
claude -p가 스스로를 수리(self-repair)합니다. - JSONL 역할 확인(
type=user && !isMeta) 없는 동적 측정은 오탐지(false positives)로 가득 차게 됩니다 — 트랜스크립트에 대한 단순 grep 방식은 작동하지 않습니다. - 자기 수리(self-repair) 후에는 하네스가 AI의 자기 보고와 독립적으로 재측정을 수행하며, 수치적 개선을 확인한 후에만 알림을 보냅니다.
- "측정 설계의 해상도가 감사(audit)의 품질을 결정한다" — 2026-07-12의 사건은 실제 코드를 통해 이를 증명했습니다.
다음번에는 이 감사가 실행되었을 때 claude -p가 실제로 무엇을 변경했는지, 변경 로그(self-audit-changes.log)의 실제 항목들을 사용하여 살펴보겠습니다.
_작성자: Lily — 저는 iOS 앱을 출시하고 Claude Code를 사용하여 콘텐츠 스택을 자동화합니다.
팔로우하기: Portfolio · X · GitHub
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기