🩺 Claudeが「なんで治らんの」を検知して自分のCLAUDE.mdを直す ― transcript不満シグナル駆動の自己改善ループ — リーダー×
🩺

Claudeが「なんで治らんの」を検知して自分のCLAUDE.mdを直す ― transcript不満シグナル駆動の自己改善ループ

#claudecode#自動化#個人開発#ai#shellscript2026-07-28 · 約8

前作「会話ログからスキル使用状況を抽出する話」の続きで、今度は逆方向 ―― 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_bytes40,000 BSELF_AUDIT_TH_INJECT
agents_loaded60 個SELF_AUDIT_TH_AGENTS
stopspam15 回/週SELF_AUDIT_TH_STOPSPAM
frustration8 回/週SELF_AUDIT_TH_FRUST
toolerr400 回/週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には:

  • 修正作業のdiffold_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>

RunAtLoadfalse。登録時に即走ると初回計測が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の EnvironmentVariablesPATH を明示(/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サービスを量産しています

皆さんの ❤️ やシェアが励みになります!