This is the next installment in my "Claude Code environment" series. Combined with the idempotent marker implementation from the previous article, I've come to realize that more than half of the reasons an autonomous loop won't stop come down to one thing: environment decay.
A Claude Code setup gets fat if you leave it alone. The rules balloon, agents you thought you'd retired come back to life, and hooks keep misfiring. The only thing you actually notice is a vague "this feels sluggish" or "it keeps making the same mistake," and then you burn time hunting down the cause. This article is about a system that measures that decay every week, lets claude -p fix it, and then confirms the improvement with an independent re-measurement after the fix.
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
.
The script's collect()
function grabs five metrics every run.
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 ' ')
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}"
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.
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.
<!-- ~/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/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.
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.find
is slowhead -200
and exclude empty files with -size +100k
, the first run of the weekend takes over 3 minutes.LANG=en_US.UTF-8
at the top, grep -E
misbehaves on multibyte characters.claude -p
isn't on the PATH~/.local/bin
. Solved by explicitly adding it to the plist's EnvironmentVariables.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 usesset -uo pipefail
, each individual measurement swallows errors with2>/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.
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--collect-only
and DRY=1
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*