{"slug": "how-claude-code-detects-its-own-weekly-rot-and-repairs-itself", "title": "How Claude Code Detects Its Own Weekly Rot and Repairs Itself", "summary": "A developer automated a weekly self-audit for Claude Code that detects silent degradation—such as bloated injected context and orphaned agent files—and hands the repair job to claude -p itself. The three-layer script measures five metrics, flags red when thresholds are crossed, and sends a single ✅ line if all is green, doubling as a low-noise liveness check.", "body_md": "Your Claude Code setup doesn't break in one dramatic moment — it degrades a few bytes at a time, and by the time you notice, you've been paying a context tax for weeks. In a [previous post](https://zenn.dev/bokuwalily/articles/daily-brief-ops-dashboard) I covered running an unattended daily health check with launchd. This one is the follow-up: **a three-layer loop that detects that quiet degradation weekly and hands the repair job to claude -p itself.**\n\nSome things in a Claude Code environment grow just from doing your normal work.\n\n`~/.claude/rules/`\n\nand `MEMORY.md`\n\nkeep getting appended to, until `.md`\n\nfiles never get archived, leaving `~/.claude/agents/`\n\npermanently loadedA performance audit on 2026-07-11 revealed that \"agents I thought I'd archived were still being injected — 99 of them,\" and that turned out to be the main cause of the degraded experience. That led to the question \"so do I have to go check this every week myself?\" — and **the answer was to automate it**, which is what `cc-self-audit.sh`\n\ndoes.\n\nThe script measures **five metrics** and flags \"red\" when any of them crosses its threshold.\n\n```\n# 閾値（env変数で上書き可）\nTH_INJECT_BYTES=\"${SELF_AUDIT_TH_INJECT:-40000}\"   # rules+CLAUDE.md+MEMORY.md 合計バイト\nTH_AGENTS=\"${SELF_AUDIT_TH_AGENTS:-60}\"            # ~/.claude/agents 配下 .md 総数（再帰）\nTH_STOPSPAM=\"${SELF_AUDIT_TH_STOPSPAM:-15}\"        # 監査hook発火/週\nTH_FRUSTRATION=\"${SELF_AUDIT_TH_FRUST:-8}\"         # 不満ワード/週\nTH_TOOLERR=\"${SELF_AUDIT_TH_TOOLERR:-400}\"         # tool失敗/週\n```\n\nThe first three are **static metrics** (state at this exact moment); the last two are **dynamic metrics** (trends since the previous run). That distinction maps directly onto how each one is measured, as described below.\n\n```\n[層1] 静的計測  → 注入bytes / agents数\n[層2] 動的計測  → hookスパム / 不満ワード / tool失敗（前回実行以降の窓）\n[層3] 閾値超過  → claude -p が ~/.claude 内を自己修正\n               → 独立再計測（自己申告は信じない）\n               → Discord #01_alerts へ報告\n```\n\nIf everything is green, it sends a single ✅ line and exits. Since it only runs once a week, it stays low-noise while also **doubling as a liveness check**.\n\n```\ninject_bytes=$(( \\\n  $(find \"$HOME/.claude/rules\" -name '*.md' -print0 2>/dev/null | xargs -0 cat 2>/dev/null | wc -c) + \\\n  $(cat \"$HOME/.claude/CLAUDE.md\" 2>/dev/null | wc -c) + \\\n  $(cat \"$HOME/CLAUDE.md\" 2>/dev/null | wc -c) + \\\n  $(cat \"$HOME/.claude/projects/-Users-matsubara/memory/MEMORY.md\" 2>/dev/null | wc -c) ))\nagents_loaded=$(find \"$HOME/.claude/agents\" -name '*.md' 2>/dev/null | wc -l | tr -d ' ')\n```\n\nMaking `find … -name '*.md'`\n\n**recursive** is deliberate: it catches agent files that have snuck into dot-directories (`.tmp/`\n\nand friends) so the problem can be detected if it recurs. If you only look at a single flat level, you'll never notice files that failed to get archived out of a subdirectory starting with `.`\n\n.\n\nBy only targeting `.jsonl`\n\nfiles newer than the marker for the previous run time (`self-audit/lastrun.marker`\n\n), it looks at **only that week's delta**.\n\n```\n[ -f \"$LASTRUN\" ] && newer=\"-newer $LASTRUN\"\nfiles=$(find \"$HOME/.claude/projects\" -maxdepth 2 -name '*.jsonl' $newer -size +100k 2>/dev/null | head -200)\n```\n\nHowever, a naive `grep`\n\nproduced **a lot of false positives** (detailed in the pitfalls section below). In the end I improved precision with inline Python.\n\n```\nSTOP_MARKER = 'Stop hook feedback:\\\\n[~/.claude/hooks/self_audit_stop.sh]: '\nFRUST_RE = re.compile(r'何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い')\n\n# stopspam: harnessが実際に注入する行のみ\nif STOP_MARKER in line:\n    stopspam += 1\n\n# frustration: isMeta でない type=user のテキストブロック（人間の実発言）のみ\nif o.get('type') != 'user' or o.get('isMeta'):\n    continue\ncontent = (o.get('message') or {}).get('content')\n# ... テキスト抽出して FRUST_RE で判定\n```\n\n`stopspam`\n\ncounts only the Stop hook lines injected by the harness. `frustration`\n\ntargets only human utterance text within the conversation (`type=user`\n\nand non-meta). That eliminated false positives from cases like \"I just read the hook's own source code with the `Read`\n\ntool.\"\n\nIf even one metric crosses its threshold, a repair prompt is piped into `claude -p`\n\n.\n\n```\nOUT=$(cd \"$HOME/.claude\" && printf '%s' \"$PROMPT\" | run_capped \"$FIX_TIMEOUT\" \"$CLAUDE\" -p \\\n      --model \"$MODEL\" --output-format text \\\n      --allowedTools \"Read,Write,Edit,Bash,Grep,Glob\" \\\n      --max-turns 50 2>&1) || true\n```\n\n`--allowedTools`\n\nnarrows it to reading and writing inside `~/.claude`\n\nonly — no touching project code or plists. The prompt is handed the violations, all metrics, and the last five history entries (the trend).\n\nKnown degradation patterns and their remedies are spelled out too (excerpt):\n\n```\n- agents_loaded超過 → 退避漏れを ~/.claude/agents-archive/ へ移動\n- inject_bytes超過  → 肥大したrules/MEMORY.mdを圧縮し、フル版は rules-archive/ へ\n- stopspam超過      → ~/.claude/hooks/self_audit_stop.sh の抑制ロジックを点検\n- frustration超過   → 該当transcriptをgrepして繰り返し失敗の真因を特定\n```\n\nEach change gets appended one at a time to `logs/self-audit-changes.log`\n\nwith \"timestamp / target / reason / **how to revert**\". Requiring the revert instructions is what lets a human roll back when the automatic repair makes a bad call.\n\nWhen `claude -p`\n\nsays \"fixed it,\" that's self-reporting. Rather than using that directly in the report, it **re-measures independently with the same collect() function**.\n\n```\n# 独立再計測（自己申告は信じない）\nAFTER=$(collect)\nlog \"after: $AFTER\"\necho \"$AFTER\" >> \"$HISTORY\"\nSTILL=$(echo \"$AFTER\" | breaches)\n\nif [ -z \"$STILL\" ]; then\n  notify \"🔧 CC自己監査: 劣化検知→自己修正済み。前:[$BREACH] 後:全緑。詳細=$CHANGELOG\"\nelse\n  notify \"🚨 CC自己監査: 自己修正後も残存 [$STILL]。要確認: $LOG / $CHANGELOG\nClaude要約: $(echo \"$OUT\" | tail -3 | tr '\\n' ' ')\"\nfi\n```\n\nIf a threshold is still exceeded after the repair, it's marked as \"remaining\" and thrown back to the human. Since the notification includes the `CHANGELOG`\n\npath, you can immediately trace what was touched.\n\n```\n<key>StartCalendarInterval</key>\n<dict>\n    <key>Hour</key>      <integer>8</integer>\n    <key>Minute</key>    <integer>30</integer>\n    <key>Weekday</key>   <integer>0</integer>  <!-- 0 = 日曜 -->\n</dict>\n<key>LowPriorityIO</key>  <true/>\n<key>Nice</key>           <integer>10</integer>\n<key>ProcessType</key>    <string>Background</string>\n```\n\nEvery Sunday at 8:30. `LowPriorityIO`\n\nand `Nice 10`\n\nkeep it from getting in the way of other work. Since `RunAtLoad: false`\n\n, it doesn't fire immediately after `launchctl load`\n\n— it waits for the next calendar time.\n\nNote\n\nPutting the weekly run on Sunday is deliberate: I want the environment in good shape before Monday's work starts, and running it twice a week or more would turn the notifications into noise. Because it doubles as a liveness check, the whole premise is that \"a ✅ arrives every week even when nothing is wrong.\"\n\n`grep`\n\ninflated stopspam to 40`old_string`\n\nof an `Edit`\n\ntool call, so it was counting the body of Read/Write/Edit tool_results too. Fixed by narrowing the Stop hook line to an \"exact match on the format the harness injects\" and doing line-level checks in Python`type=user`\n\nand non-meta`agents_loaded`\n\npicks up unintended files`.md`\n\nsneak in, the count balloons. The threshold of 60 is a value with headroom above the actual agent count (and can be overridden)`claude -p`\n\nrepair path fire for the first time in production, I confirmed only collect/breach with `DRY=1`\n\n. The repair path is structurally identical to self-repair.sh, so structural risk is low`PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:~/.local/bin`\n\n. Managed separately from scripts that additionally need a fallback to nvm-managed node`claude -p`\n\nrepairs only what's inside `~/.claude`\n\n, and always records \"how to revert\" in the change logNext time I'll write about the transcript from an actual run where \"agent bloat → automatic archiving\" fired in this audit loop, plus turning history.jsonl into a trend graph.\n\n*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.\n\nFollow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*", "url": "https://wpnews.pro/news/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself", "canonical_source": "https://dev.to/bokuwalily/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself-4akb", "published_at": "2026-07-26 00:00:10+00:00", "updated_at": "2026-07-26 00:30:43.870106+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "large-language-models", "mlops"], "entities": ["Claude Code", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself", "markdown": "https://wpnews.pro/news/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself.md", "text": "https://wpnews.pro/news/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself.txt", "jsonld": "https://wpnews.pro/news/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself.jsonld"}}