🔬 Claude Codeが自分の「劣化」を週次検知して自動修復する仕組み — リーダー×
🔬

Claude Codeが自分の「劣化」を週次検知して自動修復する仕組み

#claudecode#shellscript#automation#launchd2026-07-31 · 約7

前作「毎朝の健康診断をlaunchdで全自動化する」でlaunchdによる無人ジョブを紹介しました。今回はその応用版 ―― Claude Code環境が時間とともに静かに劣化するのを週次で自動検知し、claude -p 自身に修復させる3層ループの話です。

困りごと:環境は放置すると静かに腐る

Claude Code環境には、作業するだけで少しずつ膨らむものがある。

  • ~/.claude/rules/MEMORY.md は追記され続け、いつの間にかコンテキスト注入が40KB超え
  • 試験的に作ったエージェント定義 .md が退避されず、~/.claude/agents/ 配下に数十〜百体弱が常に読み込まれる状態が続く
  • Stop hookが繰り返し発火し続けるhookスパム状態
  • 会話ログに叱責ワードが積み上がっても誰も気づかない

2026-07-11のパフォーマンス監査で「退避したはずのagentsが99体注入され続けている」ことが判明し、ユーザー体感悪化の主因だったと分かった。「俺が毎週見に行かないといけないのか?」という話になり、自動化して解決したのが cc-self-audit.sh です。

5つの劣化指標と閾値

スクリプトは5指標を計測し、それぞれの閾値を超えたら「赤」と判定する。

# 閾値(env変数で上書き可)
TH_INJECT_BYTES="${SELF_AUDIT_TH_INJECT:-40000}"   # rules+CLAUDE.md+MEMORY.md 合計バイト
TH_AGENTS="${SELF_AUDIT_TH_AGENTS:-60}"            # ~/.claude/agents 配下 .md 総数(再帰)
TH_STOPSPAM="${SELF_AUDIT_TH_STOPSPAM:-15}"        # 監査hook発火/週
TH_FRUSTRATION="${SELF_AUDIT_TH_FRUST:-8}"         # 不満ワード/週
TH_TOOLERR="${SELF_AUDIT_TH_TOOLERR:-400}"         # tool失敗/週

前3つが静的指標(今この瞬間の状態)、後2つが動的指標(前回実行以降のトレンド)。この分類が後述の計測方法に直結する。

全体設計:3層ループ

[層1] 静的計測  → 注入bytes / agents数
[層2] 動的計測  → hookスパム / 不満ワード / tool失敗(前回実行以降の窓)
[層3] 閾値超過  → claude -p が ~/.claude 内を自己修正
               → 独立再計測(自己申告は信じない)
               → Discord #01_alerts へ報告

全部緑なら ✅ 1行だけ通知して終わり。週1起動なので低ノイズで、かつ生存確認を兼ねる

層1:静的計測

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 … -name '*.md'再帰にしているのは意図的で、ドットディレクトリ(.tmp/ 等)に紛れ込んだ agents ファイルの再発検知をするためです。フラットな1階層だけ見ていると、. で始まるサブディレクトリに退避漏れがあっても気づけない。

層2:動的計測(前回実行以降の窓)

前回実行時刻のマーカー (self-audit/lastrun.marker) より新しい .jsonl だけを対象にすることで、毎週の差分だけを見る。

[ -f "$LASTRUN" ] && newer="-newer $LASTRUN"
files=$(find "$HOME/.claude/projects" -maxdepth 2 -name '*.jsonl' $newer -size +100k 2>/dev/null | head -200)

ただし素朴な grep では誤検知が多発した(後述の落とし穴に詳述)。最終的にインラインPythonで精度を上げた。

STOP_MARKER = 'Stop hook feedback:\\n[~/.claude/hooks/self_audit_stop.sh]: '
FRUST_RE = re.compile(r'何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い')

# stopspam: harnessが実際に注入する行のみ
if STOP_MARKER in line:
    stopspam += 1

# frustration: isMeta でない type=user のテキストブロック(人間の実発言)のみ
if o.get('type') != 'user' or o.get('isMeta'):
    continue
content = (o.get('message') or {}).get('content')
# ... テキスト抽出して FRUST_RE で判定

stopspam はハーネスが注入するStop hook行のみを数える。frustration は会話内の人間の発言テキスト(type=user かつ非meta)だけを対象にする。これで「フック本体のソースコードを Read ツールで読んだだけ」の誤検知を排除できた。

層3:claude -pによる自己修正

閾値を超えた指標が1つでもあると、claude -p に修正プロンプトを流し込む。

OUT=$(cd "$HOME/.claude" && printf '%s' "$PROMPT" | run_capped "$FIX_TIMEOUT" "$CLAUDE" -p \
      --model "$MODEL" --output-format text \
      --allowedTools "Read,Write,Edit,Bash,Grep,Glob" \
      --max-turns 50 2>&1) || true

--allowedTools~/.claude 内の読み書きだけに絞り、プロジェクトコードや plist には触らせない。プロンプトには違反内容・全メトリクス・直近5件の履歴(トレンド)を渡す。

既知の劣化パターンとその対処法も明示している(抜粋):

- agents_loaded超過 → 退避漏れを ~/.claude/agents-archive/ へ移動
- inject_bytes超過  → 肥大したrules/MEMORY.mdを圧縮し、フル版は rules-archive/ へ
- stopspam超過      → ~/.claude/hooks/self_audit_stop.sh の抑制ロジックを点検
- frustration超過   → 該当transcriptをgrepして繰り返し失敗の真因を特定

変更は1件ずつ logs/self-audit-changes.log に「日時/対象/理由/戻し方」を追記させる。戻し方を必須にしているのは、自動修正が誤判定した場合のロールバックを人間が行えるようにするためです。

「自己申告は信じない」——独立再計測

claude -p が「直した」と言っても、それは自己申告です。そのまま報告に使わず、同じ collect() 関数で独立再計測する。

# 独立再計測(自己申告は信じない)
AFTER=$(collect)
log "after: $AFTER"
echo "$AFTER" >> "$HISTORY"
STILL=$(echo "$AFTER" | breaches)

if [ -z "$STILL" ]; then
  notify "🔧 CC自己監査: 劣化検知→自己修正済み。前:[$BREACH] 後:全緑。詳細=$CHANGELOG"
else
  notify "🚨 CC自己監査: 自己修正後も残存 [$STILL]。要確認: $LOG / $CHANGELOG
Claude要約: $(echo "$OUT" | tail -3 | tr '\n' ' ')"
fi

修正後も閾値を超えていれば「残存」として人間に丸投げする。CHANGELOG のパスを通知に含めるので、何を触ったかがすぐ追える。

launchd設定(日曜8:30)

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>      <integer>8</integer>
    <key>Minute</key>    <integer>30</integer>
    <key>Weekday</key>   <integer>0</integer>  
</dict>
<key>LowPriorityIO</key>  <true/>
<key>Nice</key>           <integer>10</integer>
<key>ProcessType</key>    <string>Background</string>

毎週日曜8:30。LowPriorityIONice 10 で他の作業を邪魔しない設定にしている。RunAtLoad: false なので launchctl load 直後には走らず、次のカレンダー時刻を待つ。

週1を日曜にしているのは意図的です。月曜の作業開始前に環境が整っていてほしい、かつ週2以上だと通知がノイズになる——という判断。生存確認を兼ねているので「何も悪くなくても毎週✅が来る」状態が前提です。

踏んだ落とし穴

  • grep でstopspamが40に膨れた → フック本体のソースが Edit ツールの old_string に含まれており、Read/Write/Edit のtool_result本文まで数えていた。Stop hook行の文字列を「ハーネスが注入するフォーマット完全一致」に絞り、Pythonで行単位判定して解決
  • frustrationが18に見えたが実体は4 → tool_resultのRead出力(偶然対象ワードを含むファイルを開いた)を誤検知していた。type=user かつ非metaの人間発言のみに絞って解決
  • agents_loaded の再帰で意図しないファイルを拾う.md を名乗るメモが紛れ込むと数が膨らむ。threshold 60 は実際の agents 量より余裕を持たせた値(上書き可)
  • SELF_AUDIT_DRY=1 で実検証claude -p 修正経路を本番で初回発火させる前に DRY=1 で collect/breach のみ確認。修正経路は self-repair.sh と同型なので構造リスクは低い
  • launchdの最小PATHでclaudeが見つからない → plist に PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:~/.local/bin を明示。nvm管理のnodeへのフォールバックが別途必要なスクリプトと分けて管理

まとめ

  • Claude Code環境は放置すると注入bytes増加・agents肥大・hookスパムで静かに劣化する
  • 5指標(inject_bytes / agents_loaded / stopspam / frustration / toolerr)を週次計測し、閾値超過を機械で検知
  • 閾値超過時は claude -p~/.claude 内のみ修正し、変更ログに「戻し方」を必ず残す
  • 修正後は独立再計測で数値改善を検証する——AIの自己申告は信じない
  • 全緑でも ✅ 1行を週次通知して生存確認を兼ねる

次回は、この監査ループで実際に「agents肥大→自動退避」が動いた際のトランスクリプトと、history.jsonlのトレンドグラフ化を書きます。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

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