{"slug": "when-claude-detects-why-isn-t-this-fixed-yet-and-repairs-its-own-claude-md-a-by", "title": "When Claude Detects \"Why Isn't This Fixed Yet?\" and Repairs Its Own CLAUDE.md: A Self-Improvement Loop Driven by Frustration Signals in Transcripts", "summary": "A developer built a self-auditing loop for Claude Code that detects environmental degradation from conversation transcripts and triggers automatic repairs. The script, running weekly via launchd, monitors five metrics including injection size, agent count, and user frustration signals, and when thresholds are breached, it instructs Claude to fix issues autonomously. The system then independently re-verifies improvements before reporting to Discord.", "body_md": "In a [previous post](https://zenn.dev/bokuwalily/articles/transcript-skill-usage), I extracted skill usage stats *from* my conversation logs. This time I'm going the other way: a weekly loop that **reads** the transcripts to detect whether my Claude Code environment is degrading, and when a threshold is crossed, lets `claude -p`\n\nfix things itself.\n\n`~/.claude/scripts/cc-self-audit.sh`\n\n(177 lines) runs every Sunday at 8:30 via launchd. It collects five numbers, compares them against thresholds, and if everything is green it sends a one-line ✅; if anything is red, it self-repairs, independently re-measures, and reports to Discord. **About 70% of the implementation effort went into \"why a naive grep doesn't work here.\"**\n\nA performance audit on 2026-07-11 uncovered a triple whammy: agents that were supposedly moved out of the way were still being injected — 99 of them; CLAUDE.md had silently bloated to 228KB; and a Stop hook was firing dozens of times per week.\n\nEach of those is fixable on its own. The real problem was that there was **no mechanism to notice** the \"fixed it, but it broke again\" cycle. A human doing a weekly visual check doesn't last, so I made a script do it.\n\n```\nlaunchd（日曜 8:30）\n  → cc-self-audit.sh\n      ① collect()  →  5数値（静的2 + 動的3）\n      ② breaches() →  閾値超過を列挙\n      ③ 緑 → ✅通知・終了\n      ④ 赤 → claude -p（sonnet・MAX枠）が ~/.claude 内を修正\n      ⑤ collect() 独立再実行 → 数値改善を確認（自己申告は信じない）\n      ⑥ Discord #01_alerts へ報告\n```\n\n(The flow: launchd on Sunday 8:30 → ① `collect()`\n\ngathers 5 numbers (2 static + 3 dynamic) → ② `breaches()`\n\nlists threshold violations → ③ green: notify ✅ and exit → ④ red: `claude -p`\n\n(sonnet, on my MAX plan) fixes things inside `~/.claude`\n\n→ ⑤ `collect()`\n\nre-runs independently to confirm the numbers improved (self-reports are not trusted) → ⑥ report to Discord `#01_alerts`\n\n.)\n\nThresholds (overridable via env vars):\n\n| Metric | Threshold | Variable |\n|---|---|---|\n| inject_bytes | 40,000 B | `SELF_AUDIT_TH_INJECT` |\n| agents_loaded | 60 files | `SELF_AUDIT_TH_AGENTS` |\n| stopspam | 15 /week | `SELF_AUDIT_TH_STOPSPAM` |\n| frustration | 8 /week | `SELF_AUDIT_TH_FRUST` |\n| toolerr | 400 /week | `SELF_AUDIT_TH_TOOLERR` |\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\nThe `find ~/.claude/agents -name '*.md'`\n\nis deliberately recursive so it re-detects a failure mode I've already been burned by: agents that were \"archived\" into dot-prefixed subdirectories (like `.backup/`\n\n) getting loaded again. If `agents_loaded`\n\nexceeds 60, I treat it as \"the archiving has leaked.\"\n\nThis is the core of the design. Targeting \"up to 200 JSONL files over 100KB that were updated since the last run,\" it counts stopspam (actual firings of the audit hook) and frustration (the user's frustration words).\n\n```\nSTOP_MARKER = 'Stop hook feedback:\\n[~/.claude/hooks/self_audit_stop.sh]: '\nFRUST_RE = re.compile(r'何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い')\n\nfor line in fh:\n    if STOP_MARKER in line:\n        stopspam += 1\n    try:\n        o = json.loads(line)\n    except Exception:\n        continue\n    if o.get('type') != 'user' or o.get('isMeta'):\n        continue                          # ← ここが肝\n    content = (o.get('message') or {}).get('content')\n    texts = [content] if isinstance(content, str) else \\\n        [b.get('text', '') for b in content if isinstance(b, dict) and b.get('type') == 'text'] \\\n        if isinstance(content, list) else []\n    if any(FRUST_RE.search(t) for t in texts):\n        frustration += 1\n```\n\n(The regex matches Japanese phrases like \"how many times do I have to say this,\" \"give me a break,\" \"liar,\" \"why isn't this fixed yet,\" and so on; the `# ← ここが肝`\n\ncomment marks the crucial line.)\n\nOnly blocks with `type=user`\n\nthat are not `isMeta`\n\nare examined — in other words, **only messages a human actually typed**. Why that matters is the next section.\n\nThe first implementation looked like this.\n\n```\n# 旧: 素朴なgrep\nstopspam=$(echo \"$files\" | xargs grep -h -c 'self_audit_stop' 2>/dev/null | awk '{s+=$1} END{print s+0}')\nfrustration=$(echo \"$files\" | xargs grep -hE -c 'なんで治らん|いい加減' 2>/dev/null | awk '{s+=$1} END{print s+0}')\n```\n\n`history.jsonl`\n\nstill holds the measured values from back then.\n\n```\n{\"ts\":\"2026-07-11 22:37:28\",\"inject_bytes\":21798,\"agents_loaded\":48,\"stopspam\":6,\"frustration\":4,\"toolerr\":2}\n{\"ts\":\"2026-07-12 08:30:06\",\"inject_bytes\":21966,\"agents_loaded\":48,\"stopspam\":40,\"frustration\":18,\"toolerr\":67}\n{\"ts\":\"2026-07-12 08:40:03\",\"inject_bytes\":21966,\"agents_loaded\":48,\"stopspam\":58,\"frustration\":32,\"toolerr\":68}\n```\n\nThe evening of 07-11 was normal (GREEN). The morning of 07-12, stopspam jumped from 6 to 40 and frustration from 4 to 18 — RED. Then `claude -p`\n\nattempted a fix, the independent re-measurement ran, and the numbers had climbed further, to 58/32.\n\nThe cause: grep was doing a **string search over the entire JSONL**. The transcripts contained:\n\n`old_string`\n\n/`new_string`\n\n)All of it was matching. Of the 40 stopspam hits, actual hook firings were near zero; of the 18 frustration hits, only a handful were frustration words a human had actually typed.\n\nA naive string grep cannot be used against transcripts.Unless you first determine \"which role, which block\" within the JSONL, quotations, tool output, and echoes of past logs all get counted. The resolution of your measurement design determines the quality of your audit.\n\nThe fix was switching to a JSONL parser. Only text blocks with `type=user`\n\nand `isMeta: false`\n\n(messages the user actually sent) are examined, and stopspam counts only lines matching the exact fixed format the harness actually injects. That excluded quotations, tool output, and echoes of past logs entirely.\n\nThe core of the prompt passed to `claude -p`\n\nwhen things go red:\n\n```\nPROMPT=\"あなたはClaude Code環境の自己監査・自己修復エージェント。\n\n## 検知した違反\n$BREACH\n\n## 指示\n1. ~/.claude 内だけを調査・修正する（プロジェクトコード・secret・plist削除は禁止）\n2. 変更は1件ずつ ${CHANGELOG} に「日時/対象/理由/戻し方」を追記\n3. 修正後に必ず 'bash ~/.claude/scripts/cc-self-audit.sh --collect-only' を実行し改善を数値で確認\n4. 自分で安全に直せない項目は無理せず、最後に『要人間: 理由』と1行で出力\n5. 最終出力は3行以内の要約のみ\"\n```\n\n(In English, the prompt says: \"You are a self-audit / self-repair agent for the Claude Code environment. ## Detected violations: `$BREACH`\n\n. ## Instructions: 1. Investigate and fix only inside `~/.claude`\n\n(project code, secrets, and plist deletion are forbidden). 2. For each change, append date/target/reason/how-to-revert to `${CHANGELOG}`\n\n. 3. After fixing, always run `bash ~/.claude/scripts/cc-self-audit.sh --collect-only`\n\nand confirm improvement numerically. 4. Don't force items you can't safely fix yourself — end with a one-line 'Needs human: reason.' 5. Final output is a summary of at most 3 lines.\")\n\nThe important part is instruction 3 — or rather, what happens outside it. Instead of only having `claude -p`\n\nre-measure **itself**, the harness also runs `collect()`\n\nindependently and checks the numbers (from line 165):\n\n```\n# 独立再計測（自己申告は信じない）\nAFTER=$(collect)\nlog \"after: $AFTER\"\necho \"$AFTER\" >> \"$HISTORY\"\nSTILL=$(echo \"$AFTER\" | breaches)\n```\n\nIf the AI says \"fixed it\" but the numbers haven't improved, a 🚨 goes out. When you use a Claude-built tool to repair Claude's own environment, trusting only the self-report is how you get hurt.\n\nWhat the current `history.jsonl`\n\ntells us:\n\nThe first two entries are the before/after of the manual `--collect-only`\n\non 07-11; entries 3–4 are the 07-12 grep misdetection and the subsequent re-measurement. Production operation as a weekly job starts from 08-30.\n\n```\n<key>StartCalendarInterval</key><dict>\n  <key>Weekday</key><integer>0</integer>  <!-- 日曜 -->\n  <key>Hour</key><integer>8</integer>\n  <key>Minute</key><integer>30</integer>\n</dict>\n```\n\n(`Weekday 0`\n\nwith the `日曜`\n\ncomment means Sunday.)\n\n`RunAtLoad`\n\nis `false`\n\n. If the job ran immediately on registration, the first measurement would happen in the \"empty\" state right after launchd starts. Instead, the first baseline measurement is done by manually running `--collect-only`\n\nto put one line into `history.jsonl`\n\nbefore registering the job.\n\n`type=user && !isMeta`\n\n`self_audit_stop`\n\nmatch for stopspam also hit quotations of the hook script itself`collect()`\n\nindependently of the `claude -p`\n\noutput, append to `HISTORY`\n\n, and only then judge`claude`\n\nnot found under launchd's minimal PATH`PATH`\n\nexplicitly in the plist's `EnvironmentVariables`\n\n(`/opt/homebrew/bin`\n\n+ `~/.local/bin`\n\nare required)`frustration`\n\npinned at zero can look like \"nothing is happening\"`claude -p`\n\nself-repairs`type=user && !isMeta`\n\n) is riddled with false positives — Next time, I'll walk through what `claude -p`\n\nactually changed when this audit fired, using real entries from the change log (`self-audit-changes.log`\n\n).\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/when-claude-detects-why-isn-t-this-fixed-yet-and-repairs-its-own-claude-md-a-by", "canonical_source": "https://dev.to/bokuwalily/when-claude-detects-why-isnt-this-fixed-yet-and-repairs-its-own-claudemd-a-self-improvement-4pla", "published_at": "2026-07-24 05:00:11+00:00", "updated_at": "2026-07-24 05:34:12.530854+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "large-language-models"], "entities": ["Claude Code", "Discord", "Sonnet"], "alternates": {"html": "https://wpnews.pro/news/when-claude-detects-why-isn-t-this-fixed-yet-and-repairs-its-own-claude-md-a-by", "markdown": "https://wpnews.pro/news/when-claude-detects-why-isn-t-this-fixed-yet-and-repairs-its-own-claude-md-a-by.md", "text": "https://wpnews.pro/news/when-claude-detects-why-isn-t-this-fixed-yet-and-repairs-its-own-claude-md-a-by.txt", "jsonld": "https://wpnews.pro/news/when-claude-detects-why-isn-t-this-fixed-yet-and-repairs-its-own-claude-md-a-by.jsonld"}}