前作「会話ログからスキル使用状況を抽出する話」の続きで、今度は逆方向 ―― 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フックが週に数十回飛んでいた」という三重苦が発覚しました。
それぞれは直せます。問題は「直したはずがまた壊れる」サイクルに気づく仕組みがないことでした。人間が週次で目視するのは続かないので、スクリプトにやらせることにしました。
全体設計:5メトリクス × 閾値 × 独立再計測
launchd(日曜 8:30)
→ cc-self-audit.sh
① collect() → 5数値(静的2 + 動的3)
② breaches() → 閾値超過を列挙
③ 緑 → ✅通知・終了
④ 赤 → claude -p(sonnet・MAX枠)が ~/.claude 内を修正
⑤ collect() 独立再実行 → 数値改善を確認(自己申告は信じない)
⑥ Discord #01_alerts へ報告
閾値(env変数で上書き可):
| メトリクス | 閾値 | 変数 |
|---|---|---|
| inject_bytes | 40,000 B | SELF_AUDIT_TH_INJECT |
| agents_loaded | 60 個 | SELF_AUDIT_TH_AGENTS |
| stopspam | 15 回/週 | SELF_AUDIT_TH_STOPSPAM |
| frustration | 8 回/週 | SELF_AUDIT_TH_FRUST |
| toolerr | 400 回/週 | SELF_AUDIT_TH_TOOLERR |
静的計測:バイト数とagents数
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) + \
$(cat "$HOME/CLAUDE.md" 2>/dev/null | wc -c) + \
$(cat "$HOME/.claude/projects/-Users-matsubara/memory/MEMORY.md" 2>/dev/null | wc -c) ))
agents_loaded=$(find "$HOME/.claude/agents" -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
find ~/.claude/agents -name '*.md' に再帰フラグを付けているのは、ドットで始まるサブdir(.backup/ 等)に退避したはずのagentsが再ロードされる事故を再発検知するためです。agents_loaded が60を超えたら「退避漏れが起きている」と見なします。
動的計測:transcriptのJSONLを人力発言だけ読む
ここが設計の核です。「前回実行以降に更新された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:
if STOP_MARKER in line:
stopspam += 1
try:
o = json.loads(line)
except Exception:
continue
if o.get('type') != 'user' or o.get('isMeta'):
continue # ← ここが肝
content = (o.get('message') or {}).get('content')
texts = [content] if isinstance(content, str) else \
[b.get('text', '') for b in content if isinstance(b, dict) and b.get('type') == 'text'] \
if isinstance(content, list) else []
if any(FRUST_RE.search(t) for t in texts):
frustration += 1
type=user かつ isMeta でないブロックだけを見る、つまり人間が実際に打ったメッセージだけを対象にします。なぜこれが重要かは次の節で説明します。
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 が修正を試みて独立再計測すると、さらに 58/32 まで増えていました。
原因は grep がJSONL全体を文字列検索したことです。transcriptには:
-
修正作業のdiff(
old_string/new_stringにhookスクリプトのソースが丸ごと入る) - Readツールで開いたファイルの中身(たまたまフラストレーション単語が含まれるファイル)
- tool_resultのエコーバック(前回の監査出力そのものを次のセッションが引用)
これらが全部ヒットしていました。stopspam 40件のうち実際のhook発火はほぼゼロ、frustration 18件のうち人間が打ち込んだ不満ワードも数件でした。
素朴な文字列grepはtranscriptに対して使えない。 JSONLの「どの役割の、どのブロックか」を見てから判定しないと、引用・ツール出力・過去ログのエコーがすべてカウントされる。測定設計の解像度が監査の品質を決める。
修正はJSONLパーサーへの切り替えです。type=user かつ isMeta: false(ユーザーが実際に送ったメッセージ)のテキストブロックだけを対象にし、stopspamはharnessが実際に注入する固定フォーマットの行だけを数えます。これで引用・ツール出力・過去ログのエコーをすべて除外できました。
閾値超過 → 自己修正 → 独立再計測
赤のとき claude -p に渡すプロンプトの核心部分:
PROMPT="あなたはClaude Code環境の自己監査・自己修復エージェント。
## 検知した違反
$BREACH
## 指示
1. ~/.claude 内だけを調査・修正する(プロジェクトコード・secret・plist削除は禁止)
2. 変更は1件ずつ ${CHANGELOG} に「日時/対象/理由/戻し方」を追記
3. 修正後に必ず 'bash ~/.claude/scripts/cc-self-audit.sh --collect-only' を実行し改善を数値で確認
4. 自分で安全に直せない項目は無理せず、最後に『要人間: 理由』と1行で出力
5. 最終出力は3行以内の要約のみ"
重要なのはインストラクション3です。claude -p 自身に再計測をさせるのではなく、harness側でも別途 collect() を独立実行して数値を確認します(165行目〜):
# 独立再計測(自己申告は信じない)
AFTER=$(collect)
log "after: $AFTER"
echo "$AFTER" >> "$HISTORY"
STILL=$(echo "$AFTER" | breaches)
「修正した」とAIが言っても数値が改善していなければ 🚨 を送ります。Claude製ツールを使ってClaude自身の環境を直すとき、自己申告だけを信じると事故ります。
実測値の記録
現在の history.jsonl から読み取れること:
- inject_bytes: 21,798〜21,966 B(閾値40,000の半分以下、GREEN)
- agents_loaded: 48(閾値60以下、GREEN)
- 07-12の赤はfalse-positive(grep修正後は正常値に戻る)
初回2エントリが07-11の --collect-only 前後、3〜4エントリが07-12のgrep誤検知とその後の再計測です。週次ジョブとしての本番稼働は08-30以降。
launchd設定
<key>StartCalendarInterval</key><dict>
<key>Weekday</key><integer>0</integer> <!-- 日曜 -->
<key>Hour</key><integer>8</integer>
<key>Minute</key><integer>30</integer>
</dict>
RunAtLoad は false。登録時に即走ると初回計測がlaunchd起動直後の「空」状態で終わるためです。最初の基準計測は手動で --collect-only を叩いて history.jsonl に1行入れてから登録します。
踏んだ落とし穴
-
素朴なgrepがtranscript全域を誤検知 → JSONLパーサーで
type=user && !isMetaのみ対象 -
stopspamの
self_audit_stopマッチがhookスクリプト自身の引用にもヒット → harnessが実際に注入する固定マーカー文字列(フォーマット完全一致)だけ数える -
AIが「修正した」と報告しても数値が変わっていない →
claude -pの出力とは独立してcollect()を再実行しHISTORYに追記してから判定する -
launchd の最小PATHでclaudeが見つからない → plistの
EnvironmentVariablesにPATHを明示(/opt/homebrew/bin+~/.local/binが必須) -
frustrationがゼロに貼り付いて「何も起きていない」に見える → 「不満ゼロ」は吉兆であり、月次で閾値・正規表現が現実のワードセットに追いついているか確認する
まとめ
- Claude Code環境の劣化(agent過剰・注入肥大・hookスパム・ユーザー不満)を5メトリクスで週次計測し、閾値を超えたら
claude -pが自己修正する - 動的計測はJSONLのロール判定(
type=user && !isMeta)なしでは誤検知だらけになる ―― transcriptへの素朴なgrepは使えない - 自己修正後はAIの自己申告とは独立してharnessが再計測し、数値改善を確認してから通知する
- 「測定設計の解像度が監査の品質を決める」―― 2026-07-12の事故はこれを実コードで実証してくれました
次回は、この監査が発火したときclaude -pが実際に何をどう直したか、変更ログ(self-audit-changes.log)の実例を追います。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- 制作物・記事は bokuwalily.com にまとめています🖥️
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!