DEV Community

Your Claude Code Setup Gets Bloated If You Ignore It - Weekly Auto-Slimming by Watching Injected Bytes, Agent Count, and Frustration Words

The Problem: 99 "Retired" Agents Were Still Being Injected

This is a real incident I hit during a performance audit on 2026-07-11. I'd tidied up ~/.claude/agents/ several times. I'd manually moved agent definitions I no longer used into agents-archive/ and figured I was done. But when I actually measured, agents_loaded was still stuck at 99. The culprit was dot-directories (.deprecated/, .backup/, and the like). find's default behavior recurses into dot-directories too. They looked deleted, but every single one was still being loaded through the hidden directories. This real incident is what prompted me to write cc-self-audit.sh.

Five Metrics

The script's collect() function grabs five metrics every run.

# ~/.claude/scripts/cc-self-audit.sh(collect関数より)
# --- 静的 ---
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 ' ')

# --- 動的(前回実行以降のtranscriptのみ・最大200ファイル)---
stopspam=$(echo "$files" | xargs /usr/bin/grep -h -c 'セルフ監査未実施。実装' 2>/dev/null | awk '{s+=$1} END{print s+0}')
frustration=$(echo "$files" | xargs /usr/bin/grep -h -c -E '何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い' 2>/dev/null | awk '{s+=$1} END{print s+0}')
toolerr=$(echo "$files" | xargs /usr/bin/grep -h -o -c '"is_error":true' 2>/dev/null | awk '{s+=$1} END{print s+0}')
Metric Meaning Threshold
inject_bytes Total bytes of rules + CLAUDE.md + MEMORY.md 40,000
agents_loaded Total .md count under ~/.claude/agents/ (recursion including dot-dirs) 60
stopspam Number of times the audit hook fired / week 15
frustration Occurrences of frustration words in transcripts / week 8
toolerr Count of "is_error":true in transcripts / week 400

The two static metrics are proxy variables for injection cost, and the three dynamic metrics are proxy variables for experience quality. The frustration-word detection may look out of place, but if I'm scolding it repeatedly, that means it's repeating the same failure - and those failures often stem from environment problems (misfiring hooks, rotted memory, broken tools).

The thresholds can be overridden with env variables.

TH_INJECT_BYTES="${SELF_AUDIT_TH_INJECT:-40000}"
TH_AGENTS="${SELF_AUDIT_TH_AGENTS:-60}"
TH_STOPSPAM="${SELF_AUDIT_TH_STOPSPAM:-15}"
TH_FRUSTRATION="${SELF_AUDIT_TH_FRUST:-8}"
TH_TOOLERR="${SELF_AUDIT_TH_TOOLERR:-400}"

When a Threshold Is Exceeded, claude -p Fixes Itself

If every metric is under its threshold, it just posts a one-line ✅ GREEN to Discord and exits. If any is over, it calls claude -p to fix it.

BREACH=$(echo "$METRICS" | breaches)
if [ -z "$BREACH" ]; then
  log "GREEN"
  touch "$LASTRUN"
  notify "✅ CC自己監査: 正常 ($METRICS)"
  exit 0
fi
log "RED: $BREACH"
if [ "$DRY" = "1" ]; then
  log "DRY=1: 修正スキップ"; touch "$LASTRUN"; exit 0;
fi

The prompt passed to claude -p has a remediation policy embedded for each decay pattern.

PROMPT="...
## 指示
1. ~/.claude 内だけを調査・修正する(プロジェクトコード・secret・plist削除は禁止)
2. 既知の劣化パターンと正典:
   - agents_loaded超過 → ~/.claude/agents/ 配下の退避漏れ(ドットdirも読み込まれる)を ~/.claude/agents-archive/ へ移動
   - inject_bytes超過 → 肥大したrules/MEMORY.mdを圧縮(フル版は ~/.claude/rules-archive/ へ。リンクは全維持し索引整合を検証)
   - stopspam超過 → ~/.claude/hooks/self_audit_stop.sh の抑制ロジックを点検
   - frustration/toolerr超過 → 該当transcriptをgrepして繰り返し失敗の真因を特定し、hook/スキル/メモリで再発防止を仕込む
3. 変更は1件ずつ ${CHANGELOG} に「日時/対象/理由/戻し方」を追記
4. 修正後に必ず 'bash ~/.claude/scripts/cc-self-audit.sh --collect-only' を実行し改善を数値で確認
..."

I restrict it to filesystem operations only with --allowedTools "Read,Write,Edit,Bash,Grep,Glob", and cap it at max-turns 50 and a 20-minute wall clock (FIX_TIMEOUT=1200).

Note
Having claude run --collect-only itself inside the fix loop is strictly "a confirmation as part of the remediation procedure." The final verdict of the audit is made by the outer, independent re-measurement. The key to the design is not taking the inner confirmation at face value.

A "Don't Trust Self-Reporting" Fail-Closed Design

Even when claude -p says "fixed it," I don't believe it right away. The outer harness calls collect() again to re-measure the numbers.

# 独立再計測(自己申告は信じない)
AFTER=$(collect)
log "after: $AFTER"
echo "$AFTER" >> "$HISTORY"
STILL=$(echo "$AFTER" | breaches)
touch "$LASTRUN"
if [ -z "$STILL" ]; then
  notify "🔧 CC自己監査: 劣化検知→自己修正済み。前:[$BREACH] 後:全緑。詳細=$CHANGELOG"
else
  notify "🚨 CC自己監査: 自己修正後も残存 [$STILL]。要確認: $LOG / $CHANGELOG"
fi

If a violation still remains after the fix, it flies off to Discord with a 🚨 and hands it over to a human. Trust only the OS's measured values, not self-reporting - that's the core of fail-closed.

All measured values are stacked one record per line in ~/.claude/self-audit/history.jsonl, and the prompt handed to claude also includes the most recent 5 entries as a trend. By looking at the magnitude of change - "it was 30KB last week but 51KB this week" - I can distinguish a one-off outlier from a continuous increase.

Weekly Launch via launchd, Sundays at 8:30

<!-- ~/Library/LaunchAgents/com.lily.cc-self-audit.plist -->
<key>StartCalendarInterval</key><dict>
  <key>Weekday</key><integer>0</integer>
  <key>Hour</key><integer>8</integer>
  <key>Minute</key><integer>30</integer>
</dict>
<key>RunAtLoad</key><false/>

Weekday=0 is Sunday. I set RunAtLoad to false to prevent it from running immediately right after registering with launchd. I include ~/.local/bin in the environment variable PATH and place a claude shim there.

There are two modes for manual runs.

# 計測だけ(claudeを呼ばない)
~/.claude/scripts/cc-self-audit.sh --collect-only

# ドライラン(違反表示のみ・修正なし)
SELF_AUDIT_DRY=1 ~/.claude/scripts/cc-self-audit.sh

Before putting it into production, I always eyeballed the metrics with DRY=1 first, then enabled it.

Pitfalls I Hit

  • Dot-dirs get caught by find~/.claude/agents/.deprecated/ and the like became recursion targets, so agents I thought I'd retired kept getting counted. The root cause of the real incident.
  • The transcript find is slow → Unless you cap the number of files with head -200 and exclude empty files with -size +100k, the first run of the weekend takes over 3 minutes.
  • The frustration-word regex mixes in Japanese → Unless you set LANG=en_US.UTF-8 at the top, grep -E misbehaves on multibyte characters.
  • claude -p isn't on the PATH → launchd's minimal PATH was missing ~/.local/bin. Solved by explicitly adding it to the plist's EnvironmentVariables.
  • Making claude do the post-fix re-measurement internally turns it into self-grading → The outer collect() call is mandatory. The inner confirmation stays limited to "part of the remediation procedure."

Warning

As the comment says - bash 3.2 compatible, fail-open - the audit script itself is designed so that nothing breaks even if it crashes. It uses set -uo pipefail, each individual measurement swallows errors with 2>/dev/null || true, and in the worst case the measured value becomes 0 and gets treated as "GREEN." Having the main system die because of an audit failure would be putting the cart before the horse.

Summary

A Claude Code setup accumulates decay that, if left alone, can be measured across five metrics: inject_bytes, agent count, hook spam, frustration words, and tool failures. When a threshold is exceeded, claude -p fixes only what's inside ~/.claude, and records it in the change log (self-audit-changes.log) with a "how to revert" note. After the fix, an outer independent re-measurement verifies the improvement numerically. A self-reported "fixed it" alone is not treated as done. Weekly launch via launchd, Sundays at 8:30. You can manually check with --collect-only and DRY=1. The script itself is fail-open (an audit failure won't break the environment), while the target of self-repair is fail-closed (if a violation remains after the fix, it escalates to a human).

Next time, I'll write about how I trimmed down the inject_bytes overage that this audit loop detected - a pipeline that compresses the rules and auto-slims them every week.

Written by Lily - I ship iOS apps and automate my content stack with Claude Code. Follow along: Portfolio · X · GitHub

Comments

No comments yet. Start the discussion.