
Claude가 「왜 안 고쳐져」를 감지하여 자신의 CLAUDE.md를 수정하는 방식 ― transcript 불만 시그널 기반의 자기 개선 루프
요약
Claude Code의 환경 악화를 감지하고 CLAUDE.md를 스스로 수정하는 자기 개선 루프 구현 사례를 소개합니다. 대화 로그에서 사용자의 불만 시그널과 시스템 오류 횟수를 측정하여 임계값 초과 시 자동 수정을 수행합니다.
핵심 포인트
- 사용자의 불만 표현(frustration)을 정규식으로 감지하여 피드백 루프 구축
- 에이전트 중복 로드 및 파일 크기 급증 등 환경 악화 지표 모니터링
- 단순 grep의 한계를 극복한 정교한 로그 분석 메커니즘 설계
- launchd를 활용한 정기적인 자동 감사 및 자기 수정 프로세스
전작 「대화 로그로부터 스킬 사용 상황을 추출하는 이야기」의 후속편으로, 이번에는 반대 방향 ―― transcript를 읽어서 환경이 악화되지 않았는지 감지하고, 임계값을 초과하면 claude -p가 스스로 수정하는 주간 루프에 관한 이야기입니다.
~/.claude/scripts/cc-self-audit.sh
(177행)이 일요일 8:30에 launchd로 실행됩니다. 5개의 수치를 측정하여 임계값과 비교하고, 초록색이면 ✅ 1행, 빨간색이면 자기 수정 → 독립 재측정 → Discord 보고를 수행합니다. **구현의 7할은 「왜 단순한 grep을 사용할 수 없는가」**였습니다.
2026-07-11의 퍼포먼스 감사에서 「대피시켰을 터인 agents가 99개 계속 주입되고 있다」, 「CLAUDE.md가 모르는 사이에 228KB로 불어나 있었다」, 「Stop 훅(hook)이 주당 수십 번 발생하고 있었다」라는 삼중고가 발각되었습니다.
각각은 고칠 수 있습니다. 문제는 「고쳤을 터인데 다시 망가지는」 사이클을 눈치챌 메커니즘이 없다는 것이었습니다. 인간이 매주 육안으로 확인하는 것은 지속하기 어려우므로, 스크립트에게 맡기기로 했습니다.
launchd (일요일 8:30)
→ cc-self-audit.sh
① collect() → 5개 수치 (정적 2 + 동적 3)
...
임계값 (env 변수로 덮어쓰기 가능):
| 메트릭 (Metrics) | 임계값 | 변수 |
|---|---|---|
| inject_bytes | 40,000 B | SELF_AUDIT_TH_INJECT |
| ... |
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'
에 재귀 플래그를 붙인 이유는, 점(.)으로 시작하는 서브 디렉터리(sub dir) (예: .backup/ 등)로 대피시켰을 터인 agents가 재로드되는 사고를 재발 감지하기 위해서입니다. agents_loaded가 60을 넘으면 「대피 누락이 발생하고 있다」고 간주합니다.
이 부분이 설계의 핵심입니다. 「지난 실행 이후에 업데이트된 100KB 초과의 JSONL 파일 최대 200개」를 대상으로 하여, stopspam (감사 hook의 실제 발생 횟수)과 frustration (사용자의 불만 워드 수)을 집계합니다.
STOP_MARKER = 'Stop hook feedback:\n[~/.claude/hooks/self_audit_stop.sh]: '
FRUST_RE = re.compile(r'何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い')
for line in fh:
...
type=user이면서 isMeta가 아닌 블록만을 확인합니다. 즉, 인간이 실제로 입력한 메시지만을 대상으로 합니다. 왜 이것이 중요한지는 다음 절에서 설명하겠습니다.
첫 번째 구현은 다음과 같았습니다.
# 구(舊): 단순한 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가 수정을 시도하고 독립적으로 재측정했을 때는 수치가 58/32까지 더 증가해 있었습니다.
원인은 grep이 JSONL 전체를 문자열 검색 (string search) 했기 때문입니다. transcript에는 다음 내용들이 포함되어 있었습니다:
- 수정 작업의 diff (
old_string/new_string에 hook 스크립트 소스 전체가 포함됨) - Read 툴로 연 파일의 내용 (우연히 frustration 단어가 포함된 파일)
- tool_result의 에코백 (echo-back) (이전 감사 출력을 다음 세션이 인용)
이것들이 모두 히트(hit)되었습니다. stopspam 40건 중 실제 hook 발화는 거의 제로였고, frustration 18건 중 인간이 입력한 불만 단어도 몇 건뿐이었습니다.
단순한 문자열 grep은 transcript에 사용할 수 없다. JSONL의 "어떤 역할의, 어떤 블록인가"를 확인한 뒤에 판정하지 않으면, 인용·툴 출력·과거 로그의 에코가 모두 카운트됩니다. 측정 설계의 해상도가 감사의 품질을 결정합니다.
수정 사항은 JSONL 파서(parser)로의 전환입니다. type=user이면서 isMeta: false (사용자가 실제로 보낸 메시지)인 텍스트 블록만을 대상으로 하고, stopspam은 harness가 실제로 주입하는 고정 포맷의 행만 계산합니다. 이를 통해 인용·툴 출력·과거 로그의 에코를 모두 제외할 수 있었습니다.
RED 상태일 때 claude -p에 전달하는 프롬프트의 핵심 부분:
PROMPT="당신은 Claude Code 환경의 자기 감사·자기 수복 에이전트입니다.
## 검출된 위반 사항
$BREACH
...
중요한 것은 인스트럭션(instruction) 3번입니다. claude -p 스스로에게 재측정을 시키지 않고, harness 측에서도 별도로 collect()를 독립 실행하여 수치를 확인합니다 (165행~):
# 독립 재측정 (자기 보고는 믿지 않는다)
AFTER=$(collect)
log "after: $AFTER"
...
"수정했다"라고 AI가 말하더라도 수치가 개선되지 않았다면 🚨를 보냅니다. Claude 제작 툴을 사용하여 Claude 자신의 환경을 고칠 때, 자기 보고(self-reporting)만 믿으면 사고가 발생합니다.
현재 history.jsonl에서 읽을 수 있는 내용:
- inject_bytes: 21,798~21,966 B (임계값 40,000의 절반 이하, GREEN)
- agents_loaded: 48 (임계값 60 이하, GREEN)
- 07-12의 RED는 false-positive (grep 수정 후에는 정상치로 복구)
첫 2개 엔트리는 07-11의 --collect-only 전후이며, 3~4개 엔트리는 07-12의 grep 오검출 및 그 이후의 재측정입니다. 주간 작업(weekly job)으로서의 본 작업 가동은 08-30 이후입니다.
<key>StartCalendarInterval</key><dict>
<key>Weekday</key><integer>0</integer> <!-- 일요일 -->
<key>Hour</key><integer>8</integer>
...
RunAtLoad는 false입니다. 등록 시 즉시 실행되면 첫 번째 측정이 launchd 기동 직후의 "빈" 상태로 끝나기 때문입니다. 첫 기준 측정은 수동으로 --collect-only를 실행하여 history.jsonl에 한 줄을 넣은 뒤 등록합니다.
- 단순한 grep이 transcript 전역을 오검출 → JSONL 파서로
type=user && !isMeta만 대상으로 함 - stopspam의 → harness가 실제로 주입하는 고정 마커 문자열(포맷 완전 일치)만 계산.
self_audit_stop매칭이 hook 스크립트 자신의 인용에도 히트됨 - AI가 "수정했다"고 보고해도 수치가 변하지 않음 →
claude -p의 출력과는 독립적으로collect()를 재실행하고HISTORY에 추가한 뒤 판정함 - launchd의 최소 PATH에서 claude를 찾을 수 없음 → plist의
EnvironmentVariables에PATH를 명시 (/opt/homebrew/bin
~/.local/bin가 필수임) -
→ 「불만 제로」는 길조이며, 월 단위로 임계값(threshold) 및 정규 표현식이 실제 워드셋(word set)을 따라가고 있는지 확인하는 frustration 지표가 0에 붙어버려 「아무 일도 일어나지 않는 것」처럼 보일 수 있음
- Claude Code 환경의 저하(agent 과잉, 주입 비대화, hook 스팸, 사용자 불만)를 5가지 메트릭(metrics)으로 주간 측정하고, 임계값을 초과하면
claude -p가 자기 수정(self-correction)을 수행함 - 동적 측정은 JSONL의 역할 판정(
type=user && !isMeta) 없이는 오검출(false positive) 투성이가 됨 ―― transcript에 대한 단순한 grep은 사용할 수 없음 - 자기 수정 후에는 AI의 자기 보고와는 독립적으로 harness가 재측정하여, 수치 개선을 확인한 후 통지함
- "측정 설계의 해상도가 감사의 품질을 결정한다" ―― 2026-07-12의 사고는 이를 실제 코드로 입증해 주었습니다.
다음 회차에서는, 이 감사가 발화했을 때 claude -p가 실제로 무엇을 어떻게 고쳤는지, 변경 로그(self-audit-changes.log)의 실례를 추적합니다.
Lily(@bokuwalily) ― 개인 개발자. Claude Code로 자동화 기반을 구축하며, iOS 앱과 웹 서비스를 양산하고 있습니다.
- 제작물 및 기사는 bokuwalily.com에 정리되어 있습니다 🖥️
- AI로 「자고 있어도 돌아가는 시스템」을 만들어 월 120만 엔을 벌게 된 이야기는 note 유료 기사에 💰
- OSS: github.com/bokuwalily 🐙
- 최신 정보 및 문의는 X @bokuwalily 로 🌍
여러분의 ❤️와 공유는 큰 힘이 됩니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기