전작 「대화 로그로부터 스킬 사용 상황을 추출하는 이야기」의 후속편으로, 이번에는 반대 방향 ―― 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 로 🌍
여러분의 ❤️와 공유는 큰 힘이 됩니다!