前作「毎朝の健康診断を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> <!-- 0 = 日曜 -->
</dict>
<key>LowPriorityIO</key> <true/>
<key>Nice</key> <integer>10</integer>
<key>ProcessType</key> <string>Background</string>
毎週日曜8:30。LowPriorityIO と Nice 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サービスを量産しています
- 制作物・記事は bokuwalily.com にまとめています🖥️
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!