<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lily</title>
    <description>The latest articles on DEV Community by Lily (@bokuwalily).</description>
    <link>https://dev.to/bokuwalily</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024286%2Fdf479f07-d3b3-4271-8cc4-8976224e437e.png</url>
      <title>DEV Community: Lily</title>
      <link>https://dev.to/bokuwalily</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bokuwalily"/>
    <language>en</language>
    <item>
      <title>Auto-Generating an Index of Your Claude Code Custom Agents from Their Frontmatter</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Wed, 29 Jul 2026 00:00:06 +0000</pubDate>
      <link>https://dev.to/bokuwalily/auto-generating-an-index-of-your-claude-code-custom-agents-from-their-frontmatter-hk3</link>
      <guid>https://dev.to/bokuwalily/auto-generating-an-index-of-your-claude-code-custom-agents-from-their-frontmatter-hk3</guid>
      <description>&lt;p&gt;This is a continuation of my "Claude Code environment" series. In the previous post, &lt;a href="https://dev.to/bokuwalily/nightly-tail-truncation-how-i-stopped-claude-codes-logs-from-quietly-eating-my-disk-oi9"&gt;Automatically thinning conversation logs to prevent bloat&lt;/a&gt;, I introduced the basic pattern for scheduled launchd jobs. This time I'm using that same mechanism to &lt;strong&gt;automatically maintain a list of the custom agents in &lt;code&gt;~/.claude/agents/&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Dropping a single .md file into &lt;code&gt;~/.claude/agents/&lt;/code&gt; adds a custom agent, but before long you lose track of how many you have, what model each one uses, and which tools each is allowed to touch. That's exactly what happened to me with the 27 agents I now have. I tried writing an INDEX.md by hand to manage them, and of course within a few days it had drifted from reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: the index rots
&lt;/h2&gt;

&lt;p&gt;Manually updating INDEX.md every time you add a custom agent is not sustainable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You forget you added one and leave it out&lt;/li&gt;
&lt;li&gt;You change a model later and never reflect it in INDEX.md&lt;/li&gt;
&lt;li&gt;You typo a &lt;code&gt;name&lt;/code&gt; or &lt;code&gt;description&lt;/code&gt; and never notice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I concluded there was no sustainable way to manage this other than "generate it automatically," so I wrote &lt;code&gt;agents-index.sh&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output: a real INDEX.md
&lt;/h2&gt;

&lt;p&gt;Here's how the top of my current &lt;code&gt;~/.claude/agents/INDEX.md&lt;/code&gt; looks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- AUTO-GENERATED by ~/.claude/scripts/agents-index.sh — DO NOT EDIT MANUALLY --&amp;gt;&lt;/span&gt;
&lt;span class="gh"&gt;# Agents Index (27 agents · 2026-07-28 02:02)&lt;/span&gt;

| Name | Model | Description | Tools |
|------|-------|-------------|-------|
| &lt;span class="sb"&gt;`architect`&lt;/span&gt; (&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;architect.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./architect.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;) | opus | Software architecture specialist ... | ["Read", "Grep", "Glob"] |
| &lt;span class="sb"&gt;`build-error-resolver`&lt;/span&gt; (&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;build-error-resolver.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./build-error-resolver.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;) | sonnet | Build and TypeScript error resolution specialist ... | ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] |
| &lt;span class="sb"&gt;`doc-updater`&lt;/span&gt; (&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;doc-updater.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./doc-updater.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;) | haiku | Documentation and codemap specialist ... | ["Read", "Edit", "Bash", "Grep", "Glob"] |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four columns: Name, Model, Description, and Tools. You can see at a glance how the models break down across &lt;code&gt;opus&lt;/code&gt; / &lt;code&gt;sonnet&lt;/code&gt; / &lt;code&gt;haiku&lt;/code&gt;, and immediately check things like "did this agent have Bash allowed?"&lt;/p&gt;

&lt;p&gt;Running it with the &lt;code&gt;--json&lt;/code&gt; option also writes the same content to &lt;code&gt;.index.json&lt;/code&gt;, so other scripts like a cost-tracker can reuse it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the script is designed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/.claude/scripts/agents-index.sh&lt;/code&gt; is a Bash outer shell with an inline Python3 script embedded in it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;AGENTS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/agents"&lt;/span&gt;
&lt;span class="nv"&gt;INDEX_MD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGENTS_DIR&lt;/span&gt;&lt;span class="s2"&gt;/INDEX.md"&lt;/span&gt;
&lt;span class="nv"&gt;INDEX_JSON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGENTS_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.index.json"&lt;/span&gt;
&lt;span class="nv"&gt;LOGFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/agents-index.log"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs"&lt;/span&gt;

&lt;span class="nv"&gt;EMIT_JSON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"--json"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;EMIT_JSON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

python3 - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGENTS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INDEX_MD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INDEX_JSON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EMIT_JSON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGFILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
# ... (Python本体)
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason for embedding Python as a heredoc is to hand string processing off to Python while avoiding shell PATH problems. Parsing YAML in pure Bash is dangerous, and assuming &lt;code&gt;pip install pyyaml&lt;/code&gt; would only add more environment dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  A frontmatter parser with no PyYAML dependency
&lt;/h3&gt;

&lt;p&gt;Agent definitions only ever use single-level key-value pairs, so a regex plus line splitting is enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_frontmatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^---\n(.*?)\n---\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing frontmatter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# quoted string 剥がす
&lt;/span&gt;        &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;re.DOTALL&lt;/code&gt; grabs everything between the &lt;code&gt;---&lt;/code&gt; markers wholesale, then partitioning on &lt;code&gt;:&lt;/code&gt; makes everything left of the first &lt;code&gt;:&lt;/code&gt; the key. JSON array values like &lt;code&gt;tools&lt;/code&gt; are kept as plain strings and truncated at 60 characters for display.&lt;/p&gt;

&lt;h3&gt;
  
  
  Warnings for missing fields
&lt;/h3&gt;

&lt;p&gt;An agent with an empty &lt;code&gt;name&lt;/code&gt; or &lt;code&gt;description&lt;/code&gt; is useless even if it shows up in the list. After parsing, the script checks the required fields and appends a warnings section at the end of INDEX.md.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: missing &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: missing &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The end of the generated output looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## ⚠️ Validation warnings&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; foo-agent.md: missing 'description'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
Files that are missing frontmatter entirely get routed to the warnings as &lt;code&gt;"missing frontmatter"&lt;/code&gt; and don't appear in the table. I designed it to record a warning instead of failing with an error so that INDEX.md generation itself still succeeds while keeping it clear which file is broken.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  launchd config: automatic run at 6:30 on Sundays
&lt;/h2&gt;

&lt;p&gt;The StartCalendarInterval in &lt;code&gt;~/Library/LaunchAgents/com.shun.agents-index.plist&lt;/code&gt; looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;6&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;30&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Weekday&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Weekday: 0&lt;/code&gt; is Sunday. The idea is that an up-to-date list is always waiting for me before I start work at the beginning of the week.&lt;/p&gt;

&lt;p&gt;Process priority is lowered three ways: &lt;code&gt;Nice: 10&lt;/code&gt; + &lt;code&gt;LowPriorityIO: true&lt;/code&gt; + &lt;code&gt;ProcessType: Background&lt;/code&gt;. Index generation involves disk I/O, but lowering the priority of the work keeps it from affecting Claude Code's own responsiveness.&lt;/p&gt;

&lt;p&gt;Loading is a one-time &lt;code&gt;launchctl load&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl load ~/Library/LaunchAgents/com.shun.agents-index.plist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to run it right now by hand, just call it directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.claude/scripts/agents-index.sh &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="c"&gt;# → agents=27 warnings=0 → ~/.claude/agents/INDEX.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;INDEX.md itself becomes a loop target&lt;/strong&gt; → you need to exclude it with &lt;code&gt;if p.name == "INDEX.md": continue&lt;/code&gt;. Forget it and INDEX ends up reading INDEX and breaking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;|&lt;/code&gt; in a description breaks the table&lt;/strong&gt; → always run &lt;code&gt;.replace("|", "\\|")&lt;/code&gt; after parsing. It's in the real code, but I forgot it at first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tools&lt;/code&gt; arrives as a JSON array string, so it easily exceeds the 60-character limit&lt;/strong&gt; → &lt;code&gt;["Read", "Write", "Edit", "Bash", "Grep", "Glob"]&lt;/code&gt; is already close to 50 characters. Mixing in MCP tools overflows immediately, so truncation + &lt;code&gt;...&lt;/code&gt; is essential&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing both StandardOutPath and a &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; redirect in ProgramArguments in the plist gives you duplicate logs&lt;/strong&gt; → in my actual plist both point at the same file so there's no real harm, but if you're cleaning up you should consolidate to one of them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StartCalendarInterval is skipped while the Mac is asleep&lt;/strong&gt; → launchd runs it after waking, but if the machine stays asleep through Sunday you won't get an update until Monday morning. A weekly update is good enough for me so I accept this, but if you want strictness, combine it with &lt;code&gt;pmset -a wake 1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Once &lt;code&gt;~/.claude/agents/&lt;/code&gt; grows, a manually maintained INDEX.md is guaranteed to rot → automate the generation&lt;/li&gt;
&lt;li&gt;Read the frontmatter with a one-line parser that doesn't depend on PyYAML, and generate a Markdown table plus &lt;code&gt;.index.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Missing fields are appended as a warnings section at the end of INDEX.md, and the script itself doesn't stop&lt;/li&gt;
&lt;li&gt;launchd's &lt;code&gt;Weekday: 0 / Hour: 6 / Minute: 30&lt;/code&gt; runs it automatically at 6:30 on Sundays. &lt;code&gt;Nice: 10&lt;/code&gt; + &lt;code&gt;LowPriorityIO&lt;/code&gt; keeps it out of your way&lt;/li&gt;
&lt;li&gt;You can also update immediately by hand with a single &lt;code&gt;agents-index.sh --json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I plan to write about a mechanism that uses this agent list to visualize cost trends per model.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>shell</category>
      <category>macos</category>
    </item>
    <item>
      <title>Nightly tail-truncation: how I stopped Claude Code's logs from quietly eating my disk</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Tue, 28 Jul 2026 00:00:05 +0000</pubDate>
      <link>https://dev.to/bokuwalily/nightly-tail-truncation-how-i-stopped-claude-codes-logs-from-quietly-eating-my-disk-oi9</link>
      <guid>https://dev.to/bokuwalily/nightly-tail-truncation-how-i-stopped-claude-codes-logs-from-quietly-eating-my-disk-oi9</guid>
      <description>&lt;p&gt;Log files never announce themselves. They just sit there growing until something breaks. This is part of my "Claude Code environment" series — the previous post, "&lt;a href="https://dev.to/bokuwalily/my-claude-code-logs-ballooned-to-22mb-nightly-auto-trimming-with-launchd-237l"&gt;Trimming agent logs&lt;/a&gt;", touched on log bloat in general. This time it's the concrete implementation: &lt;strong&gt;a tail-truncation script that runs every night at 3:45 via launchd&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Left alone, &lt;code&gt;~/.claude/logs/&lt;/code&gt; swells quietly. Recently &lt;code&gt;agentmemory.err.log&lt;/code&gt; hit 5.3MB in a single week, and the week before that &lt;code&gt;hook-latency.jsonl&lt;/code&gt; and &lt;code&gt;vault-auto-ingest.log&lt;/code&gt; both blew past 5MB at the same time, pushing the whole directory to 20M. Because monitoring scripts hold onto files with &lt;code&gt;tail -f&lt;/code&gt;, &lt;strong&gt;the logrotate approach of creating a new file is off the table&lt;/strong&gt;. That's exactly why I picked a method that keeps the file path fixed and only shaves the contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: logrotate changes the path
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;logrotate&lt;/code&gt;'s default behavior is "rename the old file to &lt;code&gt;.1&lt;/code&gt; and create a new file." &lt;code&gt;tail -f&lt;/code&gt; and &lt;code&gt;inotifywait&lt;/code&gt;, which hold the file descriptor via the inode, keep following it — but scripts that read by path (Claude Code's status line, cron, autopilot) never look at the new file, so &lt;strong&gt;the log appears empty&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Rewriting the path settings on the script side after every rotation isn't something you can sustain operationally. So I went with "fixed path, keep only the tail."&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: keep the tail with &lt;code&gt;tail&lt;/code&gt;, overwrite with &lt;code&gt;mv&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;These three lines are the heart of &lt;code&gt;~/.claude/scripts/log-rotate.sh&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;  &lt;span class="c"&gt;# 5MB超えたら切詰め対象&lt;/span&gt;
&lt;span class="nv"&gt;KEEP_LINES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000

&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEEP_LINES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Measure the byte count with &lt;code&gt;stat&lt;/code&gt; and target only files over 5MB&lt;/li&gt;
&lt;li&gt;Write the last 2000 lines out to a temporary file with &lt;code&gt;tail -n 2000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Overwrite the original file with the temp file via &lt;code&gt;mv&lt;/code&gt; → &lt;strong&gt;the inode doesn't change, and neither does the path&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reason it's &lt;code&gt;&amp;gt; "$f.tmp" &amp;amp;&amp;amp; mv&lt;/code&gt; rather than redirecting straight into &lt;code&gt;&amp;gt; "$f"&lt;/code&gt; is to prevent the file from ending up empty if the process crashes mid-write.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;logrotate -c&lt;/code&gt;'s &lt;code&gt;copytruncate&lt;/code&gt; option can also "copy, then truncate the original," but if a session writes heavily between the copy and the truncation, you lose up to a few seconds' worth of log lines. &lt;code&gt;tail + mv&lt;/code&gt; has no such window, which makes it the safer option here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The full script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# log-rotate.sh — ~/.claude/logs の肥大防止（毎日03:45 launchd）&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;LOGDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs"&lt;/span&gt;
&lt;span class="nv"&gt;SELF_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;/log-rotate.log"&lt;/span&gt;
&lt;span class="nv"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;KEEP_LINES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] rotate start"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 1) 肥大ログの切詰め（.log / .err / .out / .jsonl）&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.log'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.err'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.out'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.jsonl'&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;sz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;%z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sz&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAX_BYTES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEEP_LINES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] truncated &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="nv"&gt;$sz&lt;/span&gt;&lt;span class="s2"&gt; bytes -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;%z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; bytes)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;

&lt;span class="c"&gt;# 2) 日付付き成果物・マーカー・バックアップの30日超削除&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'daily-brief-2*.md'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'project-health-2*.md'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'.*-done-*'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'.vault-ingest-*'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.plist.bak*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +30 &lt;span class="nt"&gt;-delete&lt;/span&gt; 2&amp;gt;/dev/null

&lt;span class="c"&gt;# 3) 自分自身のログも肥大防止&lt;/span&gt;
&lt;span class="nv"&gt;sz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;%z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sz&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAX_BYTES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEEP_LINES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] rotate done (dir=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-maxdepth 1&lt;/code&gt; keeps it from descending into subdirectories, so only files directly under &lt;code&gt;~/.claude/logs/&lt;/code&gt; are targeted. Subdirectories sometimes hold project data for other purposes, so this is insurance against accidental deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running quietly at night with launchd
&lt;/h2&gt;

&lt;p&gt;Here's the relevant part of &lt;code&gt;~/Library/LaunchAgents/com.shun.log-rotate.plist&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;LowPriorityIO&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Nice&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProcessType&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;Background&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;45&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three settings together create the "quiet nighttime run."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LowPriorityIO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowers disk I/O priority so it doesn't cut in on other processes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Nice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lowers CPU priority (0 is the default; higher positive values mean lower priority)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProcessType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Background&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;macOS manages it under the background quality-of-service class&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;StartCalendarInterval&lt;/code&gt; is set to 3:45 because autopilot starts at 5:00, and the intent is to finish the rollup just before that. Jobs scheduled while the Mac is asleep get skipped, so if the lid is closed overnight it won't run that day. I've accepted that trade-off.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
You can set up a wake schedule with &lt;code&gt;pmset -g sched&lt;/code&gt;, but waking the machine every morning is bad for the battery even while charging. I chose to tolerate skips during sleep and protect the battery instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deleting dated artifacts older than 30 days
&lt;/h2&gt;

&lt;p&gt;Alongside truncation, the job deletes dated files matching &lt;code&gt;-mtime +30&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'daily-brief-2*.md'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'project-health-2*.md'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'.*-done-*'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'.vault-ingest-*'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.plist.bak*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +30 &lt;span class="nt"&gt;-delete&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;daily-brief-2*.md&lt;/code&gt; and &lt;code&gt;project-health-2*.md&lt;/code&gt; are artifacts that Claude Code's automated tasks emit daily. Even at a few KB to a few dozen KB per file, stacking them up every day adds up to several GB a year. Once they're past 30 days, they've served their purpose and get deleted. &lt;code&gt;.*-done-*&lt;/code&gt; and &lt;code&gt;.vault-ingest-*&lt;/code&gt; are checkpoint files from cron jobs, and those also pile up into the hundreds if left alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actual truncation records
&lt;/h2&gt;

&lt;p&gt;Here are recent results from &lt;code&gt;~/.claude/logs/log-rotate.log&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[2026-07-19 03:45:05] truncated hook-latency.jsonl (5376787 bytes -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;178061 bytes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;[2026-07-19 03:45:05] truncated vault-auto-ingest.log (5255158 bytes -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;105277 bytes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[2026-07-19 03:45:05] rotate done (dir=8.1M)

&lt;/span&gt;&lt;span class="gp"&gt;[2026-07-27 03:45:05] truncated agentmemory.err.log (5590723 bytes -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;297105 bytes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[2026-07-27 03:45:06] rotate done (dir= 16M)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hook-latency.jsonl&lt;/code&gt; went from 5.1MB to 174KB, and &lt;code&gt;vault-auto-ingest.log&lt;/code&gt; from 5.0MB to 103KB. Those two files alone cut that day's directory in half, from 17M to 8.1M. In the most recent run on 2026-07-27, &lt;code&gt;agentmemory.err.log&lt;/code&gt; had reached 5.3MB and was brought down to 290KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crashed mid-write with a direct &lt;code&gt;&amp;gt; "$f"&lt;/code&gt; redirect&lt;/strong&gt; → switched to &lt;code&gt;mv&lt;/code&gt; via a &lt;code&gt;tmp&lt;/code&gt; file so it's atomic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;find&lt;/code&gt; recursed under &lt;code&gt;.claude/&lt;/code&gt; and nearly deleted project data&lt;/strong&gt; → limited it to the top level with &lt;code&gt;-maxdepth 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;launchd's minimal PATH meant the script couldn't find &lt;code&gt;stat&lt;/code&gt;&lt;/strong&gt; → spelled out &lt;code&gt;/opt/homebrew/bin&lt;/code&gt; in the plist's &lt;code&gt;EnvironmentVariables&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jobs got skipped while the Mac slept, leaving logs untouched for days&lt;/strong&gt; → accepted the skips and kept a one-command manual &lt;code&gt;launchctl kickstart&lt;/code&gt; ready for when the machine is awake&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realized my own &lt;code&gt;log-rotate.log&lt;/code&gt; bloats too&lt;/strong&gt; → added self-truncation at the end of the script (already built in as the third section of the script above)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.claude/logs/&lt;/code&gt; accumulates &lt;code&gt;.log&lt;/code&gt; / &lt;code&gt;.err&lt;/code&gt; / &lt;code&gt;.out&lt;/code&gt; / &lt;code&gt;.jsonl&lt;/code&gt; files and passes 20M if left alone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instead of logrotate creating a new file, overwrite with &lt;code&gt;tail -n 2000 &amp;gt; tmp &amp;amp;&amp;amp; mv tmp $f&lt;/code&gt;&lt;/strong&gt; → the file path never changes, so monitoring scripts don't break&lt;/li&gt;
&lt;li&gt;launchd's &lt;code&gt;LowPriorityIO + Nice=10 + ProcessType=Background&lt;/code&gt; makes it run quietly at 3:45 AM&lt;/li&gt;
&lt;li&gt;The same job cleans up dated artifacts older than 30 days — &lt;code&gt;daily-brief-*.md&lt;/code&gt; and friends — with &lt;code&gt;find -mtime +30 -delete&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time: how I reuse the session history accumulated in these logs as long-term memory — leading into &lt;a href="https://dev.to/bokuwalily/turning-conversation-logs-into-obsidian-long-term-memory-a-pipeline-that-distills-them-every-night-18c2"&gt;turning conversation logs into long-term memory&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>launchd</category>
      <category>bash</category>
      <category>automation</category>
    </item>
    <item>
      <title>My Claude Code Logs Ballooned to 22MB — Nightly Auto-Trimming with launchd</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Mon, 27 Jul 2026 05:00:05 +0000</pubDate>
      <link>https://dev.to/bokuwalily/my-claude-code-logs-ballooned-to-22mb-nightly-auto-trimming-with-launchd-237l</link>
      <guid>https://dev.to/bokuwalily/my-claude-code-logs-ballooned-to-22mb-nightly-auto-trimming-with-launchd-237l</guid>
      <description>&lt;p&gt;Nobody plans for log files. You wire up a background job, it works, and six weeks later a directory you never look at is quietly eating your disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/bokuwalily/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself-4akb"&gt;In my previous post on automating self-audits&lt;/a&gt; I built the skeleton of a nightly job. This post is about the side effect of that: &lt;strong&gt;the logs my resident agents keep spewing grew without bound&lt;/strong&gt;, and how I designed a trimming script wired into launchd to keep things quiet.&lt;/p&gt;

&lt;p&gt;Once you have 20+ agents running as residents, logs pile up by hundreds of KB per day. Measured this morning (right after the 03:45 automated run), &lt;code&gt;~/.claude/logs/&lt;/code&gt; was &lt;strong&gt;20M, and 21M as of now (10:43)&lt;/strong&gt;. 188 files. It was only a matter of time before it crossed 22MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: disk gets heavier every morning at startup
&lt;/h2&gt;

&lt;p&gt;The more launchd jobs there are, the more logs there are. Looking at the representative large files makes it obvious.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5.7M  agentmemory.err.log
3.8M  vault-auto-ingest.log
3.1M  hook-latency.jsonl
2.7M  cost-log.jsonl
 728K dotfiles-snapshot.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of them &lt;strong&gt;just streams errors and progress forever, and I almost never read the old lines&lt;/strong&gt;. All I need is "what happened recently." And yet the logs keep accumulating, and the number from &lt;code&gt;du -sh ~/.claude/logs/&lt;/code&gt; jumps every day.&lt;/p&gt;

&lt;p&gt;Plain deletion is a problem — if the log is empty when I'm chasing down a recent error, I'm stuck. So instead of deleting, I went with &lt;strong&gt;keeping only the tail&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design: don't delete, keep only the last 2000 lines
&lt;/h2&gt;

&lt;p&gt;The policy of &lt;code&gt;log-rotate.sh&lt;/code&gt; fits in three lines.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Over 5MB → &lt;code&gt;tail -n 2000&lt;/code&gt; and overwrite&lt;/strong&gt; (retain the most recent context)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dated artifacts, markers, and .bak files → delete if older than 30 days&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The script's own log gets the same treatment&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I use the word "rotate," but it isn't rotation. It just truncates the file. No backups either. The latest 2000 lines are plenty to trace a recent failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The log-rotate.sh implementation
&lt;/h2&gt;

&lt;p&gt;Here is the full text of &lt;code&gt;~/.claude/scripts/log-rotate.sh&lt;/code&gt;. It fits in 60 lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# log-rotate.sh — ~/.claude/logs の肥大防止（毎日03:45 launchd）&lt;/span&gt;
&lt;span class="c"&gt;# 方針: 5MB超のログは末尾2000行だけ残して切詰め / 30日超の日付付き成果物・マーカー・bakは削除&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;LOGDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs"&lt;/span&gt;
&lt;span class="nv"&gt;SELF_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;/log-rotate.log"&lt;/span&gt;
&lt;span class="nv"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;KEEP_LINES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] rotate start"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 1) 肥大ログの切詰め（.log / .err / .out / .jsonl）&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.log'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.err'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.out'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.jsonl'&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;sz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;%z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sz&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAX_BYTES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEEP_LINES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] truncated &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="nv"&gt;$sz&lt;/span&gt;&lt;span class="s2"&gt; bytes -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;%z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; bytes)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;

&lt;span class="c"&gt;# 2) 日付付き成果物・マーカー・バックアップの30日超削除&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'daily-brief-2*.md'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'project-health-2*.md'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'.*-done-*'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'.vault-ingest-*'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.plist.bak*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="se"&gt;\)&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +30 &lt;span class="nt"&gt;-delete&lt;/span&gt; 2&amp;gt;/dev/null

&lt;span class="c"&gt;# 3) 自分自身のログも肥大防止&lt;/span&gt;
&lt;span class="nv"&gt;sz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;%z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sz&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAX_BYTES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEEP_LINES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] rotate done (dir=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SELF_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Truncation results are recorded in &lt;code&gt;log-rotate.log&lt;/code&gt;. Here's actual output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[2026-07-12 03:45:05] truncated agentmemory.err.log (5449070 bytes -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;331336 bytes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;[2026-07-19 03:45:05] truncated hook-latency.jsonl (5376787 bytes -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;178061 bytes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;[2026-07-19 03:45:05] truncated vault-auto-ingest.log (5255158 bytes -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;105277 bytes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[2026-07-26 03:45:05] rotate done (dir=20M)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agentmemory.err.log&lt;/code&gt; shrank from 5.2MB → 324KB, and &lt;code&gt;hook-latency.jsonl&lt;/code&gt; from 5.1MB → 174KB.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
The reason for the two-step &lt;code&gt;tail -n 2000 "$f" &amp;gt; "$f.tmp" &amp;amp;&amp;amp; mv "$f.tmp" "$f"&lt;/code&gt; is to prevent a redirect to the same file from blowing away its contents. Never use a direct &lt;code&gt;&amp;gt; "$f"&lt;/code&gt; overwrite.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The launchd configuration
&lt;/h2&gt;

&lt;p&gt;Here is the full text of &lt;code&gt;~/Library/LaunchAgents/com.shun.log-rotate.plist&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;com.shun.log-rotate&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/bin/bash&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;~/.claude/scripts/log-rotate.sh&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;45&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;LowPriorityIO&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Nice&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProcessType&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;Background&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StandardErrorPath&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;~/.claude/logs/log-rotate.err&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things matter here.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LowPriorityIO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Drops I/O to background priority so it doesn't block foreground work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Nice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Lowers CPU priority (normally 0; larger values mean lower priority)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProcessType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Background&lt;/td&gt;
&lt;td&gt;A declaration to the macOS scheduler, used in its thermal, battery, and memory pressure calculations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I set the start time to 03:45 to &lt;strong&gt;avoid the cluster of other jobs around 03:30&lt;/strong&gt;. Autopilot runs at 05:00 and daily-brief at 06:00, so log cleanup gets done quietly before them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
launchd &lt;strong&gt;skips&lt;/strong&gt; runs while the machine is asleep. If it was sleeping, the job won't run until the next matching time. If you want it to run reliably every morning, either enable wake on scheduler with &lt;code&gt;pmset -a wake 1&lt;/code&gt;, or line up your non-sleeping hours with 03:45.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How the directory size evolved
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;log-rotate.log&lt;/code&gt; retains the size from each morning. Extracting July's progression gives this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[2026-07-03] rotate done (dir=6.3M)
[2026-07-06] rotate done (dir=9.5M)
[2026-07-07] rotate done (dir=13M)
[2026-07-11] rotate done (dir=17M)
[2026-07-12] truncated agentmemory.err.log → rotate done (dir=13M)  ← 初回切詰め
[2026-07-15] rotate done (dir=15M)
[2026-07-19] truncated 2本  → rotate done (dir=8.1M)  ← 2回目
[2026-07-25] rotate done (dir=18M)
[2026-07-26] rotate done (dir=20M)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 07-12, when truncation ran, it dropped from 19M → 13M; on 07-19, from 17M → 8.1M. It keeps accumulating until a truncation target exceeds 5MB, then shrinks all at once the moment it does — a sawtooth pattern. The current 21M reflects SELF_LOG approaching 5MB, so it should shrink again at tomorrow's 03:45.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct overwrite with &lt;code&gt;&amp;gt; "$f"&lt;/code&gt; → the file ends up empty&lt;/strong&gt; — the shell processes the redirect first. The file gets truncated before &lt;code&gt;tail&lt;/code&gt; reads it, leaving zero content. Always use the two-step tmp → mv.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Without launchd's &lt;code&gt;StandardErrorPath&lt;/code&gt;, stderr disappears&lt;/strong&gt; — script errors get silently discarded. Catch them in a separate file, &lt;code&gt;log-rotate.err&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stat -f%z&lt;/code&gt; is macOS-only&lt;/strong&gt; — on Linux it's &lt;code&gt;stat -c%s&lt;/code&gt;. This plist is macOS launchd-specific so it's not an issue here, but be careful when taking the script to another environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;find -mtime +30&lt;/code&gt; is based on modification time&lt;/strong&gt; — it's 30 days since last modification, not 30 days since creation. Files that keep getting updated won't be deleted (as intended).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropping &lt;code&gt;maxdepth 1&lt;/code&gt; pulls in files in subdirectories too&lt;/strong&gt; — &lt;code&gt;~/.claude/logs/&lt;/code&gt; is flat right now, but that would cause accidents once I add subdirectories in the future. &lt;code&gt;-maxdepth 1&lt;/code&gt; is mandatory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Nice 10&lt;/code&gt; alone still stalls when I/O gets heavy&lt;/strong&gt; — always combine it with &lt;code&gt;LowPriorityIO&lt;/code&gt;. Nice is CPU, LowPriorityIO is disk; they're separate axes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;With 20+ automation agents running as residents, &lt;code&gt;~/.claude/logs/&lt;/code&gt; balloons past 21M if left alone (188 files)&lt;/li&gt;
&lt;li&gt;Choosing "&lt;strong&gt;over 5MB → truncate, keeping only the last 2000 lines&lt;/strong&gt;" instead of "delete" curbs the growth while retaining the most recent context&lt;/li&gt;
&lt;li&gt;Combining &lt;code&gt;LowPriorityIO=true&lt;/code&gt; + &lt;code&gt;Nice=10&lt;/code&gt; + &lt;code&gt;ProcessType=Background&lt;/code&gt; in launchd makes it run with low CPU and I/O load during the quiet hours after wake&lt;/li&gt;
&lt;li&gt;Truncation follows a &lt;strong&gt;sawtooth&lt;/strong&gt; pattern — it shrinks all at once the moment it crosses 5MB, then fills up again. The 2000-line window turned out to be deep enough for tracing recent failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time I plan to write about the other reason these logs pile up — &lt;strong&gt;digging into the root cause of why &lt;code&gt;agentmemory.err.log&lt;/code&gt; keeps hitting 5MB so frequently&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>launchd</category>
      <category>macos</category>
    </item>
    <item>
      <title>How Claude Code Detects Its Own Weekly Rot and Repairs Itself</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Sun, 26 Jul 2026 00:00:10 +0000</pubDate>
      <link>https://dev.to/bokuwalily/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself-4akb</link>
      <guid>https://dev.to/bokuwalily/how-claude-code-detects-its-own-weekly-rot-and-repairs-itself-4akb</guid>
      <description>&lt;p&gt;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 &lt;a href="https://dev.to/bokuwalily/automating-a-daily-morning-health-check-for-your-claude-code-setup-with-launchd-5f2d"&gt;previous post&lt;/a&gt; I covered running an unattended daily health check with launchd. This one is the follow-up: &lt;strong&gt;a three-layer loop that detects that quiet degradation weekly and hands the repair job to &lt;code&gt;claude -p&lt;/code&gt; itself.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: environments rot quietly if you leave them alone
&lt;/h2&gt;

&lt;p&gt;Some things in a Claude Code environment grow just from doing your normal work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.claude/rules/&lt;/code&gt; and &lt;code&gt;MEMORY.md&lt;/code&gt; keep getting appended to, until &lt;strong&gt;context injection quietly crosses 40KB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Experimental agent definition &lt;code&gt;.md&lt;/code&gt; files never get archived, leaving &lt;strong&gt;dozens to nearly a hundred files under &lt;code&gt;~/.claude/agents/&lt;/code&gt; permanently loaded&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Stop hooks fire over and over, creating a &lt;strong&gt;hook spam&lt;/strong&gt; condition&lt;/li&gt;
&lt;li&gt;Frustration-signaling words pile up in conversation logs and nobody notices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 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 &lt;strong&gt;the answer was to automate it&lt;/strong&gt;, which is what &lt;code&gt;cc-self-audit.sh&lt;/code&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five degradation metrics and their thresholds
&lt;/h2&gt;

&lt;p&gt;The script measures &lt;strong&gt;five metrics&lt;/strong&gt; and flags "red" when any of them crosses its threshold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 閾値（env変数で上書き可）&lt;/span&gt;
&lt;span class="nv"&gt;TH_INJECT_BYTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_INJECT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;40000&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# rules+CLAUDE.md+MEMORY.md 合計バイト&lt;/span&gt;
&lt;span class="nv"&gt;TH_AGENTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_AGENTS&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;60&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;            &lt;span class="c"&gt;# ~/.claude/agents 配下 .md 総数（再帰）&lt;/span&gt;
&lt;span class="nv"&gt;TH_STOPSPAM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_STOPSPAM&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;        &lt;span class="c"&gt;# 監査hook発火/週&lt;/span&gt;
&lt;span class="nv"&gt;TH_FRUSTRATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_FRUST&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;8&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;         &lt;span class="c"&gt;# 不満ワード/週&lt;/span&gt;
&lt;span class="nv"&gt;TH_TOOLERR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_TOOLERR&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;400&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;         &lt;span class="c"&gt;# tool失敗/週&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first three are &lt;strong&gt;static metrics&lt;/strong&gt; (state at this exact moment); the last two are &lt;strong&gt;dynamic metrics&lt;/strong&gt; (trends since the previous run). That distinction maps directly onto how each one is measured, as described below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall design: a three-layer loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[層1] 静的計測  → 注入bytes / agents数
[層2] 動的計測  → hookスパム / 不満ワード / tool失敗（前回実行以降の窓）
[層3] 閾値超過  → claude -p が ~/.claude 内を自己修正
               → 独立再計測（自己申告は信じない）
               → Discord #01_alerts へ報告
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is green, it sends a single ✅ line and exits. Since it only runs once a week, it stays low-noise while also &lt;strong&gt;doubling as a liveness check&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: static measurement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;inject_bytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/rules"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; 2&amp;gt;/dev/null | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/CLAUDE.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/CLAUDE.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/projects/-Users-matsubara/memory/MEMORY.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;agents_loaded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/agents"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Making &lt;code&gt;find … -name '*.md'&lt;/code&gt; &lt;strong&gt;recursive&lt;/strong&gt; is deliberate: it catches agent files that have snuck into dot-directories (&lt;code&gt;.tmp/&lt;/code&gt; and 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 &lt;code&gt;.&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: dynamic measurement (the window since the last run)
&lt;/h2&gt;

&lt;p&gt;By only targeting &lt;code&gt;.jsonl&lt;/code&gt; files newer than the marker for the previous run time (&lt;code&gt;self-audit/lastrun.marker&lt;/code&gt;), it looks at &lt;strong&gt;only that week's delta&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LASTRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;newer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-newer &lt;/span&gt;&lt;span class="nv"&gt;$LASTRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/projects"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 2 &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.jsonl'&lt;/span&gt; &lt;span class="nv"&gt;$newer&lt;/span&gt; &lt;span class="nt"&gt;-size&lt;/span&gt; +100k 2&amp;gt;/dev/null | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-200&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, a naive &lt;code&gt;grep&lt;/code&gt; produced &lt;strong&gt;a lot of false positives&lt;/strong&gt; (detailed in the pitfalls section below). In the end I improved precision with inline Python.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;STOP_MARKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Stop hook feedback:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n[~/.claude/hooks/self_audit_stop.sh]: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;FRUST_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# stopspam: harnessが実際に注入する行のみ
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;STOP_MARKER&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stopspam&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# frustration: isMeta でない type=user のテキストブロック（人間の実発言）のみ
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;isMeta&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ... テキスト抽出して FRUST_RE で判定
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stopspam&lt;/code&gt; counts only the Stop hook lines injected by the harness. &lt;code&gt;frustration&lt;/code&gt; targets only human utterance text within the conversation (&lt;code&gt;type=user&lt;/code&gt; and non-meta). That eliminated false positives from cases like "I just read the hook's own source code with the &lt;code&gt;Read&lt;/code&gt; tool."&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: self-repair via claude -p
&lt;/h2&gt;

&lt;p&gt;If even one metric crosses its threshold, a repair prompt is piped into &lt;code&gt;claude -p&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMPT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | run_capped &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FIX_TIMEOUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--output-format&lt;/span&gt; text &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Write,Edit,Bash,Grep,Glob"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 50 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--allowedTools&lt;/code&gt; narrows it to reading and writing inside &lt;code&gt;~/.claude&lt;/code&gt; only — no touching project code or plists. The prompt is handed the violations, all metrics, and the last five history entries (the trend).&lt;/p&gt;

&lt;p&gt;Known degradation patterns and their remedies are spelled out too (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- agents_loaded超過 → 退避漏れを ~/.claude/agents-archive/ へ移動
- inject_bytes超過  → 肥大したrules/MEMORY.mdを圧縮し、フル版は rules-archive/ へ
- stopspam超過      → ~/.claude/hooks/self_audit_stop.sh の抑制ロジックを点検
- frustration超過   → 該当transcriptをgrepして繰り返し失敗の真因を特定
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each change gets appended one at a time to &lt;code&gt;logs/self-audit-changes.log&lt;/code&gt; with "timestamp / target / reason / &lt;strong&gt;how to revert&lt;/strong&gt;". Requiring the revert instructions is what lets a human roll back when the automatic repair makes a bad call.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Never trust self-reporting" — independent re-measurement
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;claude -p&lt;/code&gt; says "fixed it," that's self-reporting. Rather than using that directly in the report, it &lt;strong&gt;re-measures independently with the same &lt;code&gt;collect()&lt;/code&gt; function&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 独立再計測（自己申告は信じない）&lt;/span&gt;
&lt;span class="nv"&gt;AFTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;collect&lt;span class="si"&gt;)&lt;/span&gt;
log &lt;span class="s2"&gt;"after: &lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HISTORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;STILL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | breaches&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STILL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;notify &lt;span class="s2"&gt;"🔧 CC自己監査: 劣化検知→自己修正済み。前:[&lt;/span&gt;&lt;span class="nv"&gt;$BREACH&lt;/span&gt;&lt;span class="s2"&gt;] 後:全緑。詳細=&lt;/span&gt;&lt;span class="nv"&gt;$CHANGELOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;notify &lt;span class="s2"&gt;"🚨 CC自己監査: 自己修正後も残存 [&lt;/span&gt;&lt;span class="nv"&gt;$STILL&lt;/span&gt;&lt;span class="s2"&gt;]。要確認: &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt; / &lt;/span&gt;&lt;span class="nv"&gt;$CHANGELOG&lt;/span&gt;&lt;span class="s2"&gt;
Claude要約: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a threshold is still exceeded after the repair, it's marked as "remaining" and thrown back to the human. Since the notification includes the &lt;code&gt;CHANGELOG&lt;/code&gt; path, you can immediately trace what was touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  launchd config (Sundays at 8:30)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;      &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;8&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;    &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;30&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Weekday&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;   &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- 0 = 日曜 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;LowPriorityIO&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;  &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Nice&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;           &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProcessType&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;Background&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Sunday at 8:30. &lt;code&gt;LowPriorityIO&lt;/code&gt; and &lt;code&gt;Nice 10&lt;/code&gt; keep it from getting in the way of other work. Since &lt;code&gt;RunAtLoad: false&lt;/code&gt;, it doesn't fire immediately after &lt;code&gt;launchctl load&lt;/code&gt; — it waits for the next calendar time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
Putting 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."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;grep&lt;/code&gt; inflated stopspam to 40&lt;/strong&gt; → the hook's own source was included in the &lt;code&gt;old_string&lt;/code&gt; of an &lt;code&gt;Edit&lt;/code&gt; tool 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&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;frustration looked like 18 but was actually 4&lt;/strong&gt; → it was falsely matching Read output in tool_results (a file that happened to contain the target words got opened). Fixed by narrowing to human utterances only: &lt;code&gt;type=user&lt;/code&gt; and non-meta&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive &lt;code&gt;agents_loaded&lt;/code&gt; picks up unintended files&lt;/strong&gt; → if notes that call themselves &lt;code&gt;.md&lt;/code&gt; sneak in, the count balloons. The threshold of 60 is a value with headroom above the actual agent count (and can be overridden)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real verification with SELF_AUDIT_DRY=1&lt;/strong&gt; → before letting the &lt;code&gt;claude -p&lt;/code&gt; repair path fire for the first time in production, I confirmed only collect/breach with &lt;code&gt;DRY=1&lt;/code&gt;. The repair path is structurally identical to self-repair.sh, so structural risk is low&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;launchd's minimal PATH can't find claude&lt;/strong&gt; → the plist explicitly sets &lt;code&gt;PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:~/.local/bin&lt;/code&gt;. Managed separately from scripts that additionally need a fallback to nvm-managed node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Left alone, a Claude Code environment quietly degrades through &lt;strong&gt;growing injection bytes, agent bloat, and hook spam&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Measure five metrics (inject_bytes / agents_loaded / stopspam / frustration / toolerr) weekly and detect threshold breaches mechanically&lt;/li&gt;
&lt;li&gt;On a breach, &lt;code&gt;claude -p&lt;/code&gt; repairs only what's inside &lt;code&gt;~/.claude&lt;/code&gt;, and always records "how to revert" in the change log&lt;/li&gt;
&lt;li&gt;After the repair, verify the numbers actually improved with &lt;strong&gt;independent re-measurement&lt;/strong&gt; — don't trust an AI's self-report&lt;/li&gt;
&lt;li&gt;Even when everything is green, send a weekly one-line ✅ so it &lt;strong&gt;doubles as a liveness check&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next 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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>shellscript</category>
      <category>automation</category>
      <category>launchd</category>
    </item>
    <item>
      <title>Automating a Daily Morning Health Check for Your Claude Code Setup with launchd</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Sat, 25 Jul 2026 00:00:14 +0000</pubDate>
      <link>https://dev.to/bokuwalily/automating-a-daily-morning-health-check-for-your-claude-code-setup-with-launchd-5f2d</link>
      <guid>https://dev.to/bokuwalily/automating-a-daily-morning-health-check-for-your-claude-code-setup-with-launchd-5f2d</guid>
      <description>&lt;p&gt;In my previous post, &lt;a href="https://dev.to/bokuwalily/the-bigger-your-claudemd-gets-the-slower-claude-code-runs-measuring-context-injection-bytes-j84"&gt;Monitoring Claude Code hook watchdogs with launchd&lt;/a&gt;, I set up liveness monitoring for hooks — and immediately ran into the next question: a healthy hook means nothing if the product behind it is down. What I really wanted was a single page I could skim in five minutes every morning and know that &lt;em&gt;everything&lt;/em&gt; is fine.&lt;/p&gt;

&lt;p&gt;That page is &lt;code&gt;daily-brief.sh&lt;/code&gt;. launchd runs it three times a day (8:00, 10:30, and at login), and it compiles production HTTP probes, hook latency p95, launchd exit codes, 7-day API costs broken down by model, and per-project git status into one Markdown file appended to Obsidian.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: checking five places by hand every morning
&lt;/h2&gt;

&lt;p&gt;The more you automate, the higher the risk that something breaks silently. I used to open all of these manually every morning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Vercel dashboard (production liveness)&lt;/li&gt;
&lt;li&gt;launchctl logs (scheduled job failures)&lt;/li&gt;
&lt;li&gt;Claude Code cost usage&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git status&lt;/code&gt; for each project&lt;/li&gt;
&lt;li&gt;The hook latency JSONL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just opening them took 3–5 minutes. Two incidents slipped through unnoticed (2026-06-11: GitHub Scout silently going blank, and a server configuration error in the autolike license API). Consolidating everything into one automatically delivered page makes missing things physically impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall design: 3 triggers → 1 Markdown file → append to Obsidian
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;launchd
  ├─ StartCalendarInterval: 8:00
  ├─ StartCalendarInterval: 10:30
  └─ RunAtLoad: true（ログイン時）
        ↓
  ~/.claude/scripts/daily-brief.sh
        ↓
  ~/.claude/logs/daily-brief-YYYYMMDD.md        ← 正本ログ
  ~/.claude/logs/daily-brief-latest.md          ← 最新コピー
  ~/Desktop/Daily Brief/today-brief-YYYYMMDD.md
  ~/Documents/claude-obsidian/wiki/briefs/daily/today-brief-YYYYMMDD.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when the second run fires at 10:30, the marker &lt;code&gt;&amp;lt;!-- daily-brief YYYYMMDD --&amp;gt;&lt;/code&gt; prevents duplicate appends (details below). The script opens like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# launchd で毎日 8:00 / 10:30 / ログイン時 実行（再実行してもマーカーで二重追記しない）。&lt;/span&gt;
&lt;span class="c"&gt;# 注意: Desktop / ~/Documents(vault) は TCC 保護領域 → plist は /bin/bash 直起動（FDA付与済み）。&lt;/span&gt;
&lt;span class="c"&gt;#       /bin/zsh 経由だと FDA 未付与で書き込みに失敗する。&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/daily-brief-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production HTTP probes: 3 retries + a 15-second timeout
&lt;/h2&gt;

&lt;p&gt;Vercel cold starts can delay the first request, so a single one-shot curl produces false alarms as &lt;code&gt;000&lt;/code&gt; (timeout). Here's the implementation of &lt;code&gt;probe_url()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;probe_url&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;000 attempt
  &lt;span class="c"&gt;# Vercel等のコールドスタートで初回が遅い→単発だと000/タイムアウトの誤報。&lt;/span&gt;
  &lt;span class="c"&gt;# 最大3回リトライ＋timeout延長で潰す。&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;attempt &lt;span class="k"&gt;in &lt;/span&gt;1 2 3&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 15 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;000&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;:0:1&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;2
  &lt;span class="k"&gt;done
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;:0:1&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- ✅ **&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;**: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- ❌ **&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;**: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;無応答&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    flag_red &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 本番が &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;無応答&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the combination of &lt;code&gt;--max-time 15&lt;/code&gt;, &lt;code&gt;sleep 2&lt;/code&gt;, and 3 retries finally eliminated the false alarms. In today's run (2026-07-24 08:10), all four production apps — the job-hunting tracker, the job-hunting navigator, the graduation planner, and AETHERIA RPG — came back 200 OK.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;probe_autolike()&lt;/code&gt; goes all the way through the Gumroad billing flow. It hits the API with a dummy key: if the response is "license not found," things are healthy (the API is alive); if it's "server configuration error," the env vars are missing, which means Pro billing is dead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;probe_autolike&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt; resp
  &lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 8 &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://autolike-license-server.vercel.app/api/verify &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;key&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;HEALTHCHECK-DUMMY&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;product&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$resp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"サーバー設定エラー"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- ❌ **autolike/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;**: サーバー設定エラー（env未設定→Pro課金死）"&lt;/span&gt;
    flag_red &lt;span class="s2"&gt;"autolike/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; がサーバー設定エラー"&lt;/span&gt;
  &lt;span class="k"&gt;elif &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$resp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s2"&gt;"ライセンスが見つかりません|valid"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- ✅ **autolike/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;**: 課金フロー稼働（Gumroad到達OK）"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything flagged RED gets collected into a "🚨 Needs attention" section at the top of the brief. Today it caught two items — a GitHub Scout scoring failure and a missing audit log in the affiliate auto-poster — and I noticed both first thing in the morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hook latency p95: aggregating JSONL with Python
&lt;/h2&gt;

&lt;p&gt;Every hook execution appends one line — &lt;code&gt;{"ts":..., "hook":..., "elapsed_ms":..., "exit_code":...}&lt;/code&gt; — to &lt;code&gt;hook-latency.jsonl&lt;/code&gt;, and &lt;code&gt;hook-latency-report.sh&lt;/code&gt; aggregates it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JSONL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DAYS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
stats = collections.defaultdict(list)
...
for hook, vals in stats.items():
    vals_sorted = sorted(vals)
    n = len(vals_sorted)
    p95 = vals_sorted[min(n-1, int(n*0.95))]
    rows.append((hook, n, sum(vals_sorted)//n, p95, vals_sorted[-1], fail.get(hook, 0)))
rows.sort(key=lambda r: -r[3])  # p95 降順（遅いものを上に）

flag = " ⚠" if p95 &amp;gt; 1500 else ""
print(f"{hook:&amp;lt;32} {n:&amp;gt;5} {mean:&amp;gt;6}ms {p95:&amp;gt;6}ms {mx:&amp;gt;6}ms {fl:&amp;gt;5}{flag}")
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It flags anything with p95 &amp;gt; 1500ms with a ⚠ and sorts slowest-first, so you can tell at a glance what's heavy. Here are today's actual numbers (last 24h, 356 records):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;=== hook latency (last 1d, 356 records) ===
hook                                 n    mean     p95     max  fail
----------------------------------------------------------------------
skills-auto-update.sh                8 2030404ms 3215081ms 3215081ms     0 ⚠
obsidian_context.sh                 71  61835ms  15679ms 3194281ms     0 ⚠
message_display_filter.sh           69  23740ms   9106ms 1469891ms     0 ⚠
cost_guard.sh                       68  16719ms   6789ms 1019530ms     0 ⚠
user_prompt_submit.sh               70   5516ms   6426ms 338132ms     0 ⚠
model_routing_reminder.sh           70    901ms   5703ms   6386ms     0 ⚠
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;obsidian_context.sh&lt;/code&gt; shows a p95 of 15679ms (~16 seconds) and a mean of 61835ms (~1 minute), but the mean is dragged up by max outliers, so p95 is what I judge by. Since the columns are sorted by p95 descending, "higher up = heavier" reads at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  launchd exit codes: every job in one line
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl list | &lt;span class="nb"&gt;grep &lt;/span&gt;com.shun | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{printf "- %s (last exit=%s)\n", $3, $2}'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today's output (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- com.shun.daily-brief (last exit=0)
- com.shun.vault-auto-ingest (last exit=0)
- com.shun.autopilot (last exit=1)
- com.shun.self-repair (last exit=0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can immediately see that only &lt;code&gt;autopilot&lt;/code&gt; is at exit=1. The other 13 jobs were all exit=0. For details I go to &lt;code&gt;~/.claude/logs/com.shun.&amp;lt;name&amp;gt;.log&lt;/code&gt;, but the brief already narrows down &lt;em&gt;where&lt;/em&gt; things are failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  7-day API cost by model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run_to 60 ~/.claude/scripts/cost-summary.sh 7 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today's actual numbers (last 7 days):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;=== cost summary (last 7d) ===&lt;/span&gt;
  &lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1323&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;$11012.43&lt;/span&gt;
  &lt;span class="na"&gt;avg/sess&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$8.324&lt;/span&gt;

  &lt;span class="na"&gt;per model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;claude-fable-5&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;56617 messages&lt;/span&gt;
    &lt;span class="na"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;43282 messages&lt;/span&gt;
    &lt;span class="na"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1882 messages&lt;/span&gt;
    &lt;span class="na"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;632 messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;$11,012 over 7 days is within my Max plan's flat rate, but the per-model message counts show where usage is skewed. fable-5's 56,617 messages exceed opus-4-8's 43,282, which is useful input for tuning model routing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;run_to&lt;/code&gt; is a wrapper that keeps a hung child script from stalling the entire brief:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TIMEOUT_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew/bin/timeout"&lt;/span&gt;
run_to&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMEOUT_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMEOUT_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--kill-after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;15 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the hook latency report and the cost summary are wrapped in &lt;code&gt;run_to 60&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking through personal projects' git status
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;repo &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ~/dev/shukatsu-tracker &lt;span class="se"&gt;\&lt;/span&gt;
  ~/dev/seo-affiliate-site &lt;span class="se"&gt;\&lt;/span&gt;
  ~/Projects/autolike-license-server &lt;span class="se"&gt;\&lt;/span&gt;
  ...&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;/.git"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="nt"&gt;--abbrev-ref&lt;/span&gt; HEAD 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;uncommitted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="nt"&gt;--porcelain&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;last_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%cr %s'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-c1-70&lt;/span&gt; | /usr/bin/iconv &lt;span class="nt"&gt;-f&lt;/span&gt; UTF-8 &lt;span class="nt"&gt;-t&lt;/span&gt; UTF-8 &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- **&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;** [&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;uncommitted&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 未コミット — _&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;last_commit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a reason for sanitizing with &lt;code&gt;iconv -f UTF-8 -t UTF-8 -c&lt;/code&gt;: &lt;code&gt;cut -c&lt;/code&gt; operates on bytes, so it can split UTF-8 multibyte characters mid-sequence, after which &lt;code&gt;grep&lt;/code&gt; treats the file as binary and marker detection breaks — a footgun I hit myself. In today's run I could immediately see that &lt;code&gt;lead-finder&lt;/code&gt; was sitting at 75 uncommitted changes with its last commit three weeks ago (it's in an intentional PAUSE state).&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotent design: comment markers prevent duplicate appends
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;daily-brief.sh&lt;/code&gt; runs twice, at 8:00 and 10:30, and sometimes &lt;code&gt;vault-auto-ingest.sh&lt;/code&gt; has already created the file first. I needed a design that appends exactly once no matter the execution order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MARK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;!-- daily-brief &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --&amp;gt;"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Desktop/Daily Brief/today-brief-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;/wiki/briefs/daily/today-brief-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARN: &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt; 作成不可（TCC?）"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="c"&gt;# ファイルがなければ placeholder を作る&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# 今日のブリーフ — &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"_（Claude生成のブリーフ本文は auto-ingest 完了時にこの上へ差し替わる）_"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARN: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt; 作成不可（TCC?）"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
  &lt;span class="c"&gt;# LC_ALL=C: 不正バイトがあっても grep がバイナリ扱いで見落とさないように&lt;/span&gt;
  &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"---"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARN: 合流追記失敗: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LC_ALL=C&lt;/code&gt; forces grep into byte mode, which prevents the case where invalid bytes in the file make &lt;code&gt;grep&lt;/code&gt; treat it as binary, return &lt;code&gt;--&lt;/code&gt;, and miss the marker. In today's vault file, &lt;code&gt;&amp;lt;!-- daily-brief 20260724 --&amp;gt;&lt;/code&gt; appears exactly once, and I confirmed the 10:30 re-run was skipped.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;vault-auto-ingest.sh&lt;/code&gt; contains the same merge logic. Whichever runs first, a matching marker means the append is skipped, so nothing gets duplicated. The comment marker works as a "receipt" for the operation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The TCC trap: &lt;code&gt;/usr/bin/env bash&lt;/code&gt; fails
&lt;/h2&gt;

&lt;p&gt;Since macOS Ventura, &lt;code&gt;~/Desktop&lt;/code&gt; and &lt;code&gt;~/Documents&lt;/code&gt; are protected by TCC (Transparency, Consent, and Control). When launchd starts your script, &lt;strong&gt;Full Disk Access is not inherited depending on the interpreter path&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ❌ /usr/bin/env 経由 → FDA未付与 → Desktop/Documentsへの書き込みがサイレントに失敗 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/usr/bin/env&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;bash&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;~/.claude/scripts/daily-brief.sh&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- ✅ /bin/bash 直起動 → FDA付与済みで動く --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/bin/bash&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;~/.claude/scripts/daily-brief.sh&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Going through &lt;code&gt;/usr/bin/env&lt;/code&gt; makes the process count as one without FDA, and both &lt;code&gt;mkdir -p&lt;/code&gt; and file writes fail silently. No error message, no files created — just empty logs. This trap once cost me a full week of missing briefs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The script's shebang &lt;code&gt;#!/usr/bin/env bash&lt;/code&gt; can stay as is. Changing only the first element of the plist's &lt;code&gt;ProgramArguments&lt;/code&gt; to &lt;code&gt;/bin/bash&lt;/code&gt; fixes it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  plist design points
&lt;/h2&gt;

&lt;p&gt;Here are the key parts extracted from the actual file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;com.shun.daily-brief&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/bin/bash&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;~/.claude/scripts/daily-brief.sh&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- 8:00 と 10:30 の2回 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;8&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;30&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- ログイン時にも即実行 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- バックグラウンドジョブがClaude Codeの応答を遅らせないために --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;LowPriorityIO&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Nice&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProcessType&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;Background&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- PATH を明示しないと homebrew/nvm/git が見つからない --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;EnvironmentVariables&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;PATH&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/path/to/.nvm/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Making &lt;code&gt;StartCalendarInterval&lt;/code&gt; an array lets you express multiple times in a single plist. If the Mac is asleep when a scheduled time passes, the job does not run at the next wake (that's just how launchd works). &lt;code&gt;RunAtLoad: true&lt;/code&gt; compensates by also firing at login.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;~&lt;/code&gt; is not expanded inside a plist. Use absolute paths, or use &lt;code&gt;$HOME&lt;/code&gt; inside the script.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No FDA when going through &lt;code&gt;/usr/bin/env&lt;/code&gt;&lt;/strong&gt; → changed the first plist element to launch &lt;code&gt;/bin/bash&lt;/code&gt; directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False &lt;code&gt;000&lt;/code&gt; alarms from Vercel cold starts&lt;/strong&gt; → fixed with 3 retries + &lt;code&gt;--max-time 15&lt;/code&gt; + &lt;code&gt;sleep 2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate output from appending &lt;code&gt;|| echo 0&lt;/code&gt; to &lt;code&gt;grep -c&lt;/code&gt;&lt;/strong&gt; → &lt;code&gt;grep -c&lt;/code&gt; already prints "0" on zero matches, so &lt;code&gt;|| echo 0&lt;/code&gt; is unnecessary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;grep treating files as binary and missing markers with &lt;code&gt;LC_ALL&lt;/code&gt; unset&lt;/strong&gt; → &lt;code&gt;LC_ALL=C grep -qF&lt;/code&gt; guarantees byte mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cut -c&lt;/code&gt; splitting UTF-8 and breaking downstream &lt;code&gt;grep&lt;/code&gt;&lt;/strong&gt; → sanitize with &lt;code&gt;iconv -f UTF-8 -t UTF-8 -c&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hung child script stalling the whole brief&lt;/strong&gt; → wrap with &lt;code&gt;timeout --kill-after=15 60 &amp;lt;cmd&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jobs skipped when the Mac sleeps through a scheduled time&lt;/strong&gt; → compensated with &lt;code&gt;RunAtLoad&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production HTTP probes&lt;/strong&gt; use 3 retries + &lt;code&gt;--max-time 15&lt;/code&gt; to prevent false alarms from Vercel cold starts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook latency p95&lt;/strong&gt; is aggregated from JSONL with Python, flagging anything over 1500ms with ⚠ and sorting slowest-first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;launchd exit codes&lt;/strong&gt; are checked across all jobs with one line: &lt;code&gt;launchctl list | grep com.shun&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7-day costs&lt;/strong&gt; include per-model message counts, feeding into model routing decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent design&lt;/strong&gt; with the &lt;code&gt;&amp;lt;!-- daily-brief YYYYMMDD --&amp;gt;&lt;/code&gt; marker means no duplicate appends regardless of order or run count&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The TCC restriction&lt;/strong&gt; is worked around by launching &lt;code&gt;/bin/bash&lt;/code&gt; directly in the plist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I'll cover the design of an assistant hook that automatically appends "yesterday's Claude Code work summary" to this brief.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>launchd</category>
      <category>obsidian</category>
    </item>
    <item>
      <title>The Bigger Your CLAUDE.md Gets, the Slower Claude Code Runs — Measuring Context Injection Bytes Weekly to Catch Degradation Early</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Fri, 24 Jul 2026 11:00:06 +0000</pubDate>
      <link>https://dev.to/bokuwalily/the-bigger-your-claudemd-gets-the-slower-claude-code-runs-measuring-context-injection-bytes-j84</link>
      <guid>https://dev.to/bokuwalily/the-bigger-your-claudemd-gets-the-slower-claude-code-runs-measuring-context-injection-bytes-j84</guid>
      <description>&lt;p&gt;This is the next installment in my "Claude Code environment" series. In the previous post on the &lt;a href="https://dev.to/bokuwalily/when-claude-detects-why-isnt-this-fixed-yet-and-repairs-its-own-claudemd-a-self-improvement-4pla"&gt;self-audit frustration loop&lt;/a&gt;, I wrote about a "never-ending improvement loop." This time I want to dig into what sits at the root of it: &lt;strong&gt;detecting and self-correcting context injection bloat&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At the start of every session, Claude Code unconditionally injects the full contents of &lt;code&gt;rules/*.md&lt;/code&gt;, &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;, &lt;code&gt;~/CLAUDE.md&lt;/code&gt;, and &lt;code&gt;MEMORY.md&lt;/code&gt;. Every time you add a rule, this byte count silently grows. In an audit on 2026-07-11, I found that &lt;strong&gt;total injection had exceeded 40KB, and 99 agents I thought I had archived were still being loaded&lt;/strong&gt; — the main cause of my performance degradation. Since then, I've kept a permanent loop running that measures this automatically every week and attempts self-correction when thresholds are exceeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: it gets fat while nobody's looking
&lt;/h2&gt;

&lt;p&gt;Adding rules to CLAUDE.md is a good thing. But "adding" only moves in one direction. After half a year, forgotten sections get duplicated, an archive you thought was compressed turns out to still be live, and files you moved to &lt;code&gt;.agents/&lt;/code&gt; keep getting loaded anyway.&lt;/p&gt;

&lt;p&gt;The core issue is that &lt;strong&gt;degradation is invisible unless you measure it&lt;/strong&gt;. Claude's gut feeling of "this seems sluggish" may be correct, but nobody routinely checks how many KB are being injected. I needed a mechanism that produces the numbers weekly and automatically attempts a fix when a human-defined threshold is exceeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to measure
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;collect()&lt;/code&gt; function in &lt;code&gt;~/.claude/scripts/cc-self-audit.sh&lt;/code&gt; assembles the metrics. There are two axes: static and dynamic measurement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static measurement: injection bytes and agent count
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;inject_bytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/rules"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; 2&amp;gt;/dev/null | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/CLAUDE.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/CLAUDE.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/projects/-Users-matsubara/memory/MEMORY.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;agents_loaded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/agents"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;find ... -name '*.md'&lt;/code&gt; recurses into dot directories too. Even if you think "I moved them to &lt;code&gt;agents-archive/&lt;/code&gt;, so it's fine," if that directory lives under &lt;code&gt;~/.claude/agents/&lt;/code&gt; as a dot directory, the files &lt;strong&gt;keep getting loaded&lt;/strong&gt; — that's the trap (this is exactly how 99 agents survived on 2026-07-11).&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic measurement: stopspam, frustration words, tool failures
&lt;/h3&gt;

&lt;p&gt;Three metrics are aggregated from the transcripts (&lt;code&gt;.jsonl&lt;/code&gt;) since the last run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;STOP_MARKER &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Stop hook feedback:\\n[~/.claude/hooks/self_audit_stop.sh]: '&lt;/span&gt;
FRUST_RE &lt;span class="o"&gt;=&lt;/span&gt; re.compile&lt;span class="o"&gt;(&lt;/span&gt;r&lt;span class="s1"&gt;'何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;stopspam counts only "lines that the Stop hook actually injected into the prompt." A naive grep also picks up the same string when it happens to appear inside a tool_result, inflating the number 6–10x over reality (I fixed exactly this on 2026-07-12, correcting 40 → 6). For frustration, only text blocks of &lt;code&gt;type=user&lt;/code&gt; that are not &lt;code&gt;isMeta&lt;/code&gt; — i.e., the human's own raw utterances — are counted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thresholds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TH_INJECT_BYTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_INJECT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;40000&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# rules+CLAUDE.md+MEMORY.md 合計&lt;/span&gt;
&lt;span class="nv"&gt;TH_AGENTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_AGENTS&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;60&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;            &lt;span class="c"&gt;# ~/.claude/agents 配下 .md 総数&lt;/span&gt;
&lt;span class="nv"&gt;TH_STOPSPAM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_STOPSPAM&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;        &lt;span class="c"&gt;# 監査hook発火/週&lt;/span&gt;
&lt;span class="nv"&gt;TH_FRUSTRATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_FRUST&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;8&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;         &lt;span class="c"&gt;# 不満ワード/週&lt;/span&gt;
&lt;span class="nv"&gt;TH_TOOLERR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_TOOLERR&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;400&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;         &lt;span class="c"&gt;# tool失敗/週&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They're overridable via environment variables, so tuning them later never requires touching the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breach detection → self-repair → independent re-measurement
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;breaches()&lt;/code&gt; lists the items that exceeded their thresholds and hands them to &lt;code&gt;claude -p&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BREACH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$METRICS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | breaches&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BREACH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;log &lt;span class="s2"&gt;"GREEN"&lt;/span&gt;
  notify &lt;span class="s2"&gt;"✅ CC自己監査: 正常 (&lt;/span&gt;&lt;span class="nv"&gt;$METRICS&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When there are breaches, the prompt restricts fixes to inside &lt;code&gt;~/.claude&lt;/code&gt; only (touching project code, secrets, or deleting plists is explicitly forbidden).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROMPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"あなたはClaude Code環境の自己監査・自己修復エージェント。週次監査で劣化を検知した。
...
1. ~/.claude 内だけを調査・修正する（プロジェクトコード・secret・plist削除は禁止）
2. 既知の劣化パターンと正典:
   - agents_loaded超過 → ~/.claude/agents/ 配下の退避漏れを ~/.claude/agents-archive/ へ移動
   - inject_bytes超過 → 肥大したrules/MEMORY.mdを圧縮（フル版は ~/.claude/rules-archive/ へ）
   ...
4. 修正後に必ず 'bash ~/.claude/scripts/cc-self-audit.sh --collect-only' を実行し改善を数値で確認
..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;claude -p&lt;/code&gt; finishes, the script itself calls &lt;code&gt;collect()&lt;/code&gt; &lt;strong&gt;once more for an independent re-measurement&lt;/strong&gt;. Claude's self-reporting is not trusted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 独立再計測（自己申告は信じない）&lt;/span&gt;
&lt;span class="nv"&gt;AFTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;collect&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;STILL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | breaches&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STILL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;notify &lt;span class="s2"&gt;"🔧 CC自己監査: 劣化検知→自己修正済み。前:[&lt;/span&gt;&lt;span class="nv"&gt;$BREACH&lt;/span&gt;&lt;span class="s2"&gt;] 後:全緑。"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;notify &lt;span class="s2"&gt;"🚨 CC自己監査: 自己修正後も残存 [&lt;/span&gt;&lt;span class="nv"&gt;$STILL&lt;/span&gt;&lt;span class="s2"&gt;]。要確認: &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When everything is green, it sends &lt;code&gt;✅ a single line&lt;/code&gt;; breach followed by a successful fix sends &lt;code&gt;🔧 with before/after numbers&lt;/code&gt;; breaches remaining after the fix sends &lt;code&gt;🚨&lt;/code&gt; — all to the &lt;code&gt;#01_alerts&lt;/code&gt; channel on Discord. Since it's weekly, it never gets noisy — a low-noise setup that doubles as a liveness check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it via launchd on Sundays at 8:30 AM
&lt;/h2&gt;

&lt;p&gt;Here's the core of &lt;code&gt;~/Library/LaunchAgents/com.lily.cc-self-audit.plist&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;8&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;30&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Weekday&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;   &lt;span class="c"&gt;&amp;lt;!-- 0=日曜 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Nice&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;LowPriorityIO&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProcessType&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;Background&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Nice=10&lt;/code&gt; and &lt;code&gt;LowPriorityIO=true&lt;/code&gt; are there because &lt;code&gt;claude -p&lt;/code&gt; can run for up to 1200 seconds (the default value of &lt;code&gt;FIX_TIMEOUT&lt;/code&gt;). To avoid squeezing the Mac's foreground work, the job is explicitly treated as background.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you don't set &lt;code&gt;RunAtLoad: false&lt;/code&gt;, the script runs every time you &lt;code&gt;bootstrap&lt;/code&gt; the plist. Don't forget to set &lt;code&gt;RunAtLoad&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; for weekly jobs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Integration with automation-health.sh
&lt;/h2&gt;

&lt;p&gt;The "weekly/monthly batch liveness" section of &lt;code&gt;~/.claude/scripts/automation-health.sh&lt;/code&gt; watches the modification time of &lt;code&gt;cc-self-audit.log&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nv"&gt;CRON_JOBS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  &lt;span class="s2"&gt;"weekly cleanup-misc:&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE&lt;/span&gt;&lt;span class="s2"&gt;/logs/cleanup-misc.log:192"&lt;/span&gt;
  &lt;span class="s2"&gt;"weekly env-audit:&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE&lt;/span&gt;&lt;span class="s2"&gt;/logs/env-audit-latest.md:192"&lt;/span&gt;
  ...
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;entry &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CRON_JOBS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;age_h &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-le&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$budget&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;ok &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;h前 (budget &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;budget&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;h)"&lt;/span&gt;
  &lt;span class="k"&gt;else &lt;/span&gt;wn &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;h前 (budget &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;budget&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;h 超過 — launchd 配送失敗の可能性)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;cc-self-audit.sh&lt;/code&gt; didn't run in a given week, &lt;code&gt;automation-health.sh&lt;/code&gt; emits a WARN saying "over 192h." This closes the loop of "monitoring the monitor."&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Archiving into a dot directory achieves nothing&lt;/strong&gt; — even with a dot-prefixed name like &lt;code&gt;.agents-backup/&lt;/code&gt;, &lt;code&gt;find -name '*.md'&lt;/code&gt; still picks it up. You either need to place &lt;code&gt;agents-archive/&lt;/code&gt; &lt;strong&gt;outside&lt;/strong&gt; &lt;code&gt;~/.claude/&lt;/code&gt;, or specify an exclusion path in the &lt;code&gt;agents_loaded&lt;/code&gt; measurement expression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False grep matches inflated dynamic metrics 10x&lt;/strong&gt; — transcripts embed "the source of that very script" as tool_result content. Use an exact match on STOP_MARKER plus a python3 JSON parse to filter down to user blocks that are not isMeta&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating self-reporting as completion&lt;/strong&gt; — even when &lt;code&gt;claude -p&lt;/code&gt; says "I compressed inject_bytes," the numbers can't be confirmed without an independent re-measurement via &lt;code&gt;collect()&lt;/code&gt;. Always fetch the post-fix score with &lt;code&gt;--collect-only&lt;/code&gt; and record it in &lt;code&gt;HISTORY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting FIX_TIMEOUT too short interrupts the fix&lt;/strong&gt; — the default 1200 seconds (20 minutes) was almost always enough, but when there's a lot to fix, it times out midway and &lt;code&gt;STILL&lt;/code&gt; remains non-empty. In that case, the &lt;code&gt;🚨&lt;/code&gt; notification hands the decision to a human&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;launchd can't find claude without an explicit PATH&lt;/strong&gt; — put both &lt;code&gt;/opt/homebrew/bin&lt;/code&gt; and &lt;code&gt;~/.local/bin&lt;/code&gt; in the plist's EnvironmentVariables. launchd's PATH is a different beast from your interactive shell's&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code's injected byte count can be measured with &lt;code&gt;find ... | wc -c&lt;/code&gt;. &lt;strong&gt;Degradation you don't measure is degradation you can't see&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Start from thresholds of &lt;code&gt;TH_INJECT_BYTES=40000&lt;/code&gt; / &lt;code&gt;TH_AGENTS=60&lt;/code&gt;, then tune via env overrides to fit your own environment&lt;/li&gt;
&lt;li&gt;Delegate fixes to &lt;code&gt;claude -p&lt;/code&gt; scoped to &lt;code&gt;~/.claude&lt;/code&gt; only, then &lt;strong&gt;independently re-measure&lt;/strong&gt; immediately afterward to verify its self-reporting&lt;/li&gt;
&lt;li&gt;The launchd job at 8:30 AM on Sundays plus &lt;code&gt;automation-health.sh&lt;/code&gt; gives you double-layered monitoring that can detect "the monitoring itself is dead"&lt;/li&gt;
&lt;li&gt;Once a week, one line of output — no notification fatigue. A streak of green becomes the proof of health&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I plan to write about how to compress the "bloated MEMORY.md" this loop detects while &lt;strong&gt;keeping the index consistent&lt;/strong&gt; throughout.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>devops</category>
      <category>performance</category>
    </item>
    <item>
      <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</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Fri, 24 Jul 2026 05:00:11 +0000</pubDate>
      <link>https://dev.to/bokuwalily/when-claude-detects-why-isnt-this-fixed-yet-and-repairs-its-own-claudemd-a-self-improvement-4pla</link>
      <guid>https://dev.to/bokuwalily/when-claude-detects-why-isnt-this-fixed-yet-and-repairs-its-own-claudemd-a-self-improvement-4pla</guid>
      <description>&lt;p&gt;In a &lt;a href="https://dev.to/bokuwalily/parse-transcriptjsonl-directly-to-see-whats-actually-being-called-41bm"&gt;previous post&lt;/a&gt;, I extracted skill usage stats &lt;em&gt;from&lt;/em&gt; my conversation logs. This time I'm going the other way: a weekly loop that &lt;strong&gt;reads&lt;/strong&gt; the transcripts to detect whether my Claude Code environment is degrading, and when a threshold is crossed, lets &lt;code&gt;claude -p&lt;/code&gt; fix things itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;~/.claude/scripts/cc-self-audit.sh&lt;/code&gt; (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. &lt;strong&gt;About 70% of the implementation effort went into "why a naive grep doesn't work here."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: nobody was detecting environment degradation
&lt;/h2&gt;

&lt;p&gt;A 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.&lt;/p&gt;

&lt;p&gt;Each of those is fixable on its own. The real problem was that there was &lt;strong&gt;no mechanism to notice&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall design: 5 metrics × thresholds × independent re-measurement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;launchd（日曜 8:30）
  → cc-self-audit.sh
      ① collect()  →  5数値（静的2 + 動的3）
      ② breaches() →  閾値超過を列挙
      ③ 緑 → ✅通知・終了
      ④ 赤 → claude -p（sonnet・MAX枠）が ~/.claude 内を修正
      ⑤ collect() 独立再実行 → 数値改善を確認（自己申告は信じない）
      ⑥ Discord #01_alerts へ報告
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The flow: launchd on Sunday 8:30 → ① &lt;code&gt;collect()&lt;/code&gt; gathers 5 numbers (2 static + 3 dynamic) → ② &lt;code&gt;breaches()&lt;/code&gt; lists threshold violations → ③ green: notify ✅ and exit → ④ red: &lt;code&gt;claude -p&lt;/code&gt; (sonnet, on my MAX plan) fixes things inside &lt;code&gt;~/.claude&lt;/code&gt; → ⑤ &lt;code&gt;collect()&lt;/code&gt; re-runs independently to confirm the numbers improved (self-reports are not trusted) → ⑥ report to Discord &lt;code&gt;#01_alerts&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;Thresholds (overridable via env vars):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;inject_bytes&lt;/td&gt;
&lt;td&gt;40,000 B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELF_AUDIT_TH_INJECT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agents_loaded&lt;/td&gt;
&lt;td&gt;60 files&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELF_AUDIT_TH_AGENTS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stopspam&lt;/td&gt;
&lt;td&gt;15 /week&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELF_AUDIT_TH_STOPSPAM&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frustration&lt;/td&gt;
&lt;td&gt;8 /week&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELF_AUDIT_TH_FRUST&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toolerr&lt;/td&gt;
&lt;td&gt;400 /week&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELF_AUDIT_TH_TOOLERR&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Static metrics: injected bytes and agent count
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;inject_bytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/rules"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; 2&amp;gt;/dev/null | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/CLAUDE.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/CLAUDE.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/projects/-Users-matsubara/memory/MEMORY.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;agents_loaded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/agents"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;find ~/.claude/agents -name '*.md'&lt;/code&gt; is deliberately recursive so it re-detects a failure mode I've already been burned by: agents that were "archived" into dot-prefixed subdirectories (like &lt;code&gt;.backup/&lt;/code&gt;) getting loaded again. If &lt;code&gt;agents_loaded&lt;/code&gt; exceeds 60, I treat it as "the archiving has leaked."&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic metrics: reading only human-typed messages out of the transcript JSONL
&lt;/h2&gt;

&lt;p&gt;This 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).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;STOP_MARKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Stop hook feedback:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[~/.claude/hooks/self_audit_stop.sh]: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;FRUST_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;STOP_MARKER&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stopspam&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;isMeta&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;                          &lt;span class="c1"&gt;# ← ここが肝
&lt;/span&gt;    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; \
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; \
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FRUST_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;frustration&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(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 &lt;code&gt;# ← ここが肝&lt;/code&gt; comment marks the crucial line.)&lt;/p&gt;

&lt;p&gt;Only blocks with &lt;code&gt;type=user&lt;/code&gt; that are not &lt;code&gt;isMeta&lt;/code&gt; are examined — in other words, &lt;strong&gt;only messages a human actually typed&lt;/strong&gt;. Why that matters is the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The false-positive incident of 2026-07-12
&lt;/h2&gt;

&lt;p&gt;The first implementation looked like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 旧: 素朴なgrep&lt;/span&gt;
&lt;span class="nv"&gt;stopspam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | xargs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'self_audit_stop'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{s+=$1} END{print s+0}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;frustration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | xargs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-hE&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'なんで治らん|いい加減'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{s+=$1} END{print s+0}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;history.jsonl&lt;/code&gt; still holds the measured values from back then.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-11 22:37:28"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"inject_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;21798&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"agents_loaded"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"stopspam"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"frustration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"toolerr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-12 08:30:06"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"inject_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;21966&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"agents_loaded"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"stopspam"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"frustration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"toolerr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-12 08:40:03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"inject_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;21966&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"agents_loaded"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"stopspam"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"frustration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"toolerr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 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 &lt;code&gt;claude -p&lt;/code&gt; attempted a fix, the independent re-measurement ran, and the numbers had climbed further, to 58/32.&lt;/p&gt;

&lt;p&gt;The cause: grep was doing a &lt;strong&gt;string search over the entire JSONL&lt;/strong&gt;. The transcripts contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diffs from the repair work itself&lt;/strong&gt; (the hook script's source landing wholesale in &lt;code&gt;old_string&lt;/code&gt;/&lt;code&gt;new_string&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contents of files opened with the Read tool&lt;/strong&gt; (files that happened to contain frustration words)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Echoed-back tool_results&lt;/strong&gt; (the previous audit's output being quoted by the next session)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A naive string grep cannot be used against transcripts.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix was switching to a JSONL parser. Only text blocks with &lt;code&gt;type=user&lt;/code&gt; and &lt;code&gt;isMeta: false&lt;/code&gt; (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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threshold breach → self-repair → independent re-measurement
&lt;/h2&gt;

&lt;p&gt;The core of the prompt passed to &lt;code&gt;claude -p&lt;/code&gt; when things go red:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROMPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"あなたはClaude Code環境の自己監査・自己修復エージェント。

## 検知した違反
&lt;/span&gt;&lt;span class="nv"&gt;$BREACH&lt;/span&gt;&lt;span class="s2"&gt;

## 指示
1. ~/.claude 内だけを調査・修正する（プロジェクトコード・secret・plist削除は禁止）
2. 変更は1件ずつ &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHANGELOG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; に「日時/対象/理由/戻し方」を追記
3. 修正後に必ず 'bash ~/.claude/scripts/cc-self-audit.sh --collect-only' を実行し改善を数値で確認
4. 自分で安全に直せない項目は無理せず、最後に『要人間: 理由』と1行で出力
5. 最終出力は3行以内の要約のみ"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(In English, the prompt says: "You are a self-audit / self-repair agent for the Claude Code environment. ## Detected violations: &lt;code&gt;$BREACH&lt;/code&gt;. ## Instructions: 1. Investigate and fix only inside &lt;code&gt;~/.claude&lt;/code&gt; (project code, secrets, and plist deletion are forbidden). 2. For each change, append date/target/reason/how-to-revert to &lt;code&gt;${CHANGELOG}&lt;/code&gt;. 3. After fixing, always run &lt;code&gt;bash ~/.claude/scripts/cc-self-audit.sh --collect-only&lt;/code&gt; and 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.")&lt;/p&gt;

&lt;p&gt;The important part is instruction 3 — or rather, what happens outside it. Instead of only having &lt;code&gt;claude -p&lt;/code&gt; re-measure &lt;strong&gt;itself&lt;/strong&gt;, the harness also runs &lt;code&gt;collect()&lt;/code&gt; independently and checks the numbers (from line 165):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 独立再計測（自己申告は信じない）&lt;/span&gt;
&lt;span class="nv"&gt;AFTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;collect&lt;span class="si"&gt;)&lt;/span&gt;
log &lt;span class="s2"&gt;"after: &lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HISTORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;STILL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | breaches&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the recorded numbers show
&lt;/h2&gt;

&lt;p&gt;What the current &lt;code&gt;history.jsonl&lt;/code&gt; tells us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;inject_bytes&lt;/strong&gt;: 21,798–21,966 B (less than half the 40,000 threshold, GREEN)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;agents_loaded&lt;/strong&gt;: 48 (under the threshold of 60, GREEN)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 07-12 red was a false positive&lt;/strong&gt; (after the grep fix, values returned to normal)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two entries are the before/after of the manual &lt;code&gt;--collect-only&lt;/code&gt; on 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  launchd configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Weekday&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- 日曜 --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;8&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;30&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;Weekday 0&lt;/code&gt; with the &lt;code&gt;日曜&lt;/code&gt; comment means Sunday.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RunAtLoad&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;. 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 &lt;code&gt;--collect-only&lt;/code&gt; to put one line into &lt;code&gt;history.jsonl&lt;/code&gt; before registering the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Naive grep misdetected across the entire transcript&lt;/strong&gt; → use a JSONL parser and target only &lt;code&gt;type=user &amp;amp;&amp;amp; !isMeta&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;self_audit_stop&lt;/code&gt; match for stopspam also hit quotations of the hook script itself&lt;/strong&gt; → count only the fixed marker string the harness actually injects (exact format match)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI reported "fixed" but the numbers hadn't changed&lt;/strong&gt; → re-run &lt;code&gt;collect()&lt;/code&gt; independently of the &lt;code&gt;claude -p&lt;/code&gt; output, append to &lt;code&gt;HISTORY&lt;/code&gt;, and only then judge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;claude&lt;/code&gt; not found under launchd's minimal PATH&lt;/strong&gt; → declare &lt;code&gt;PATH&lt;/code&gt; explicitly in the plist's &lt;code&gt;EnvironmentVariables&lt;/code&gt; (&lt;code&gt;/opt/homebrew/bin&lt;/code&gt; + &lt;code&gt;~/.local/bin&lt;/code&gt; are required)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;frustration&lt;/code&gt; pinned at zero can look like "nothing is happening"&lt;/strong&gt; → "zero frustration" is a good sign, but check monthly that the thresholds and regex still track the real-world word set&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Degradation of a Claude Code environment (agent overload, injection bloat, hook spam, user frustration) is &lt;strong&gt;measured weekly across 5 metrics&lt;/strong&gt;, and when a threshold is crossed, &lt;code&gt;claude -p&lt;/code&gt; self-repairs&lt;/li&gt;
&lt;li&gt;Dynamic measurement without JSONL role checks (&lt;code&gt;type=user &amp;amp;&amp;amp; !isMeta&lt;/code&gt;) is riddled with false positives — &lt;strong&gt;naive grep against transcripts does not work&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;After a self-repair, &lt;strong&gt;the harness re-measures independently of the AI's self-report&lt;/strong&gt; and confirms numerical improvement before notifying&lt;/li&gt;
&lt;li&gt;"The resolution of your measurement design determines the quality of your audit" — the 2026-07-12 incident demonstrated this with real code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I'll walk through what &lt;code&gt;claude -p&lt;/code&gt; actually changed when this audit fired, using real entries from the change log (&lt;code&gt;self-audit-changes.log&lt;/code&gt;).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>shell</category>
      <category>devops</category>
    </item>
    <item>
      <title>Parse transcript.jsonl Directly to See What's Actually Being Called</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Thu, 23 Jul 2026 11:00:05 +0000</pubDate>
      <link>https://dev.to/bokuwalily/parse-transcriptjsonl-directly-to-see-whats-actually-being-called-41bm</link>
      <guid>https://dev.to/bokuwalily/parse-transcriptjsonl-directly-to-see-whats-actually-being-called-41bm</guid>
      <description>&lt;p&gt;Claude Code's &lt;code&gt;/usage&lt;/code&gt; tells you how much you're spending, but not what you're spending it on. In my previous post, "&lt;a href="https://dev.to/bokuwalily/log-every-agent-invocation-building-usage-analytics-with-claude-codes-stop-hook-and-6oc"&gt;Logging Agent invocations to a file and piping them to Slack&lt;/a&gt;," I wrote about recording tool calls. This time is a follow-up: a script that &lt;strong&gt;parses the already-recorded transcript.jsonl directly and tallies how many times each Skill, each Agent, and each MCP server was called&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Claude Code's &lt;code&gt;/usage&lt;/code&gt; shows cost ($) and remaining quota, but it can't show you "which Skill did I use most this week" or "which Agent is the heaviest." Every &lt;code&gt;tool_use&lt;/code&gt; block is preserved in transcript.jsonl, so by reading it directly you can surface usage at a layer &lt;code&gt;/usage&lt;/code&gt; never tells you about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: /usage only shows cost
&lt;/h2&gt;

&lt;p&gt;What &lt;code&gt;/usage&lt;/code&gt; reports is the consumption rate for 5-hour and 7-day blocks, plus per-session token counts. It does not give you a &lt;strong&gt;per-item breakdown&lt;/strong&gt; like "this AutoTrigger Skill was actually called 20 times this week" or "the &lt;code&gt;general-purpose&lt;/code&gt; Agent accounts for half of all Agent calls."&lt;/p&gt;

&lt;p&gt;To do cost optimization or check the health of your environment, you need the line items for "what are the tokens being spent on." transcript.jsonl is the ideal source for this, and every session is accumulated under &lt;code&gt;~/.claude/projects/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structure of transcript.jsonl
&lt;/h2&gt;

&lt;p&gt;Each file is JSONL, one record per line. Each turn Claude Code sent or received becomes one record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"toolu_01M5Rw..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"file_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"caller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12043&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;421&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All you need for the tally is the &lt;code&gt;type == "tool_use"&lt;/code&gt; blocks inside &lt;code&gt;message.content[]&lt;/code&gt;. The &lt;code&gt;name&lt;/code&gt; field is the tool name, and &lt;code&gt;input&lt;/code&gt; holds the arguments.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Tally key&lt;/th&gt;
&lt;th&gt;Extracted from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Skill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;input.skill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skill name (also namespace-split for the &lt;code&gt;plugin:name&lt;/code&gt; form)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;input.subagent_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subagent type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcp__*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2nd segment of &lt;code&gt;name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MCP server name (&lt;code&gt;mcp__&amp;lt;server&amp;gt;__&amp;lt;tool&amp;gt;&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The heart of the script
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/.claude/scripts/usage-breakdown.sh&lt;/code&gt; embeds Python via a Bash heredoc. Bash receives the directory and the window period, and Python does a full scan of the files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nv"&gt;ARG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;7d&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TR_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/projects/-Users-matsubara"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TR_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"(no transcript dir)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

python3 - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TR_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ARG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import sys, json, datetime, glob, collections, os
tr_dir, arg = sys.argv[1], sys.argv[2]
SHORT = arg == "--short"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window period is filtered by the file's &lt;code&gt;mtime&lt;/code&gt;. When the &lt;code&gt;--short&lt;/code&gt; flag comes in, the parsing is all the same; only the output is narrowed down to a single line.&lt;/p&gt;

&lt;p&gt;The parsing itself is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tr_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/*.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getmtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mtime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;inp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
                &lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skill&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;skill_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skill&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;skill_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;skill_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;agent_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;mcp_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the &lt;code&gt;plugin:name&lt;/code&gt; form of Skill (e.g. &lt;code&gt;hookify:configure&lt;/code&gt;), I also split on &lt;code&gt;:&lt;/code&gt; and tally the namespace separately. This lets me see which plugin package is heavy on a different axis from the individual skill names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it for real
&lt;/h2&gt;

&lt;p&gt;Running it for the last 7 days looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;usage-breakdown.sh 7d
&lt;span class="go"&gt;=== usage breakdown (last 7d, 51 transcripts) ===

total tool_use: 4230

--- top tools ---
   2583  Bash
    567  Read
    402  Edit
    151  Write
    103  mcp__claude-in-chrome__computer
     73  mcp__claude-in-chrome__javascript_tool
     62  TaskUpdate
     50  ToolSearch
     42  mcp__claude-in-chrome__navigate
     36  TaskCreate

--- top skills (2 unique) ---
      1  harness-audit
      1  superpowers:brainstorming

--- top agents (5 unique) ---
     20  general-purpose
      7  Explore
      4  ?
      2  reviewer
      1  fork

--- top MCP servers (4 unique) ---
    261  claude-in-chrome
      3  claude_ai_Google_Calendar
      3  computer-use
      2  claude_ai_Gmail

--- top plugin namespaces (1 unique) ---
      1  superpowers
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This week had 51 sessions and 4,230 tool_use calls, with Bash accounting for 61% of the total (2,583 calls). For MCP, &lt;code&gt;claude-in-chrome&lt;/code&gt; was in a league of its own at 261 calls. For Agents, &lt;code&gt;general-purpose&lt;/code&gt; was the most frequent at 20, with Explore at 7.&lt;/p&gt;

&lt;p&gt;The reason there are only 2 Skill calls total is that this week I had a lot of setups where Skills fire via AutoTrigger (keyword matching in CLAUDE.md) rather than being invoked manually. AutoTrigger should show up in the transcript as &lt;code&gt;Skill&lt;/code&gt; tool_use, so the low count is honestly reflected.&lt;/p&gt;

&lt;p&gt;:::message&lt;br&gt;
There are 4 Agents where &lt;code&gt;subagent_type&lt;/code&gt; comes out as &lt;code&gt;"?"&lt;/code&gt;. These are calls that didn't specify &lt;code&gt;subagent_type&lt;/code&gt; on the &lt;code&gt;Agent&lt;/code&gt; tool (effectively the default general-purpose), which the script falls back to via &lt;code&gt;inp.get("subagent_type", "?")&lt;/code&gt;. In practice, when &lt;code&gt;"?"&lt;/code&gt; starts increasing, narrowing down "which Session" is useful for discovering missing subagent specifications.&lt;br&gt;
:::&lt;/p&gt;
&lt;h2&gt;
  
  
  The --short mode for embedding in the status line
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--short&lt;/code&gt; outputs a one-line summary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;usage-breakdown.sh &lt;span class="nt"&gt;--short&lt;/span&gt;
&lt;span class="go"&gt;4235 tool_use across 51 sessions (7d)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've built it into a custom script for Claude Code's status line. Placing it alongside the cost quota (the output of the previous post's &lt;code&gt;token-budget-advisor.sh&lt;/code&gt;) puts you in a state where "usage and remaining quota are visible at a glance."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# an example statusline hook&lt;/span&gt;
&lt;span class="nv"&gt;BUDGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;~/.claude/scripts/token-budget-advisor.sh &lt;span class="nt"&gt;--short&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;USAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;~/.claude/scripts/usage-breakdown.sh &lt;span class="nt"&gt;--short&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"💰 &lt;/span&gt;&lt;span class="nv"&gt;$BUDGET&lt;/span&gt;&lt;span class="s2"&gt; | 🔧 &lt;/span&gt;&lt;span class="nv"&gt;$USAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;mtime&lt;/code&gt; filter is file-granular, so it's coarse&lt;/strong&gt; — when a session spans multiple days, older turns get included in the tally as part of a "recent file." To be strict you'd need line-level filtering via &lt;code&gt;rec.get("timestamp")&lt;/code&gt;, but for grasping trends, &lt;code&gt;mtime&lt;/code&gt; was good enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't pick up JSONL in subdirectories&lt;/strong&gt; — &lt;code&gt;glob.glob(f"{tr_dir}/*.jsonl")&lt;/code&gt; only looks one flat level. Subagent transcripts go under &lt;code&gt;&amp;lt;session-uuid&amp;gt;/subagents/agent-*.jsonl&lt;/code&gt;, so they're outside the tally in the current implementation. Switching to &lt;code&gt;**/*.jsonl&lt;/code&gt; would pick them up, but it's heavy, so I've left it out intentionally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long MCP tool names like &lt;code&gt;mcp__claude-in-chrome__computer&lt;/code&gt;&lt;/strong&gt; — since the implementation takes the 2nd segment of &lt;code&gt;split("__")&lt;/code&gt;, only &lt;code&gt;parts[1]&lt;/code&gt; remains and the server name is extracted correctly. However, in cases where the server name itself contains &lt;code&gt;__&lt;/code&gt;, it will be misextracted (haven't run into it so far).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 30d window takes several minutes&lt;/strong&gt; — because it full-scans over 890 files, using &lt;code&gt;7d&lt;/code&gt; or &lt;code&gt;--short&lt;/code&gt; for daily use and only reaching for &lt;code&gt;30d&lt;/code&gt; when digging deep is the realistic approach.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you pick up just the &lt;code&gt;message.content[].type == "tool_use"&lt;/code&gt; from &lt;code&gt;transcript.jsonl&lt;/code&gt;, you can get the call counts for Skills, Agents, and MCP servers&lt;/li&gt;
&lt;li&gt;Just outputting &lt;code&gt;most_common(10)&lt;/code&gt; from a Python counter immediately shows you the "heavy items"&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;mtime&lt;/code&gt; period filter and the embedded Python heredoc fit it all into a single Bash script&lt;/li&gt;
&lt;li&gt;Making it a one-line summary with &lt;code&gt;--short&lt;/code&gt; and always displaying it in the status line lets you monitor the health of your environment on an axis separate from cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look at cost with &lt;code&gt;/usage&lt;/code&gt;, and look at the per-item breakdown with this script. Combining the two raises the resolution of "how many tokens were spent for what."&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Log Every Agent Invocation: Building Usage Analytics with Claude Code's Stop Hook and transcript_path</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Thu, 23 Jul 2026 05:00:06 +0000</pubDate>
      <link>https://dev.to/bokuwalily/log-every-agent-invocation-building-usage-analytics-with-claude-codes-stop-hook-and-6oc</link>
      <guid>https://dev.to/bokuwalily/log-every-agent-invocation-building-usage-analytics-with-claude-codes-stop-hook-and-6oc</guid>
      <description>&lt;p&gt;In the previous post in my "Claude Code environment" series, &lt;a href="https://dev.to/bokuwalily/hunting-down-zombie-agents-logging-subagent-usage-with-a-claude-code-stop-hook-332g"&gt;automatically pruning zombie agents&lt;/a&gt;, I wrote about finding agents that are defined but never used. But where did those numbers come from in the first place? This post covers exactly that: &lt;strong&gt;a mechanism that reads the &lt;code&gt;transcript_path&lt;/code&gt; included in the Stop hook payload with Python and automatically accumulates agent invocations into a JSONL log&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Right now, &lt;code&gt;~/.claude/logs/agent-invocations.jsonl&lt;/code&gt; holds &lt;strong&gt;553&lt;/strong&gt; recorded invocations. Over the last 7 days, &lt;code&gt;general-purpose&lt;/code&gt; was called 58 times and &lt;code&gt;Explore&lt;/code&gt; 24 times. And the fact that &lt;strong&gt;47 agents were never called even once&lt;/strong&gt; — that was invisible without this mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: "Am I actually using this agent?"
&lt;/h2&gt;

&lt;p&gt;As agent definitions pile up under &lt;code&gt;~/.claude/agents/&lt;/code&gt;, you lose track of "when did I last use this?" — even though you wrote them yourself. Unused definitions are actively harmful: they only add to context injection.&lt;/p&gt;

&lt;p&gt;Claude Code has a mechanism that fires a Stop hook when a session ends, and its payload includes &lt;code&gt;transcript_path&lt;/code&gt;. This is the path to the log file for the entire conversation. If you read it, you can extract everything: which agent was called, when, and how long it took.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overall flow
&lt;/h2&gt;

&lt;p&gt;Session ends → Stop hook fires → &lt;code&gt;stop_hooks_combined.sh&lt;/code&gt; receives the payload → passes it to &lt;code&gt;stop_agent_tracker.sh&lt;/code&gt; → Python reads the transcript and appends to the JSONL.&lt;/p&gt;

&lt;p&gt;Here is how the actual &lt;code&gt;~/.local/bin/stop_hooks_combined.sh&lt;/code&gt; is wired up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# stdin → tmpfile に保存して複数 hook へ順次渡す&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PAYLOAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;hook &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/hooks/stop_notify.sh"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/hooks/stop_cost_log.sh"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/hooks/stop_agent_tracker.sh"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/hooks/stop_session_summary.sh"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.discord/stop_post_session.sh"&lt;/span&gt;
&lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hook&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hook&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PAYLOAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Stop hook receives its payload on stdin. To fan it out to multiple hooks, the payload goes through a tmpfile, and each hook gets it via a &lt;code&gt;&amp;lt; "$PAYLOAD"&lt;/code&gt; redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing &lt;code&gt;stop_agent_tracker.sh&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The script has two layers: a bash wrapper and inline Python.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;LOG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs"&lt;/span&gt;
&lt;span class="nv"&gt;OUT_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_DIR&lt;/span&gt;&lt;span class="s2"&gt;/agent-invocations.jsonl"&lt;/span&gt;

&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;STOP_INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OUT_LOG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
# ...
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bash side just reads stdin and sets environment variables. All the actual work is delegated to Python.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 1: scan the entire transcript and build an index
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;uses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;    &lt;span class="c1"&gt;# tool_use_id -&amp;gt; (ts, name, input, caller)
&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;# tool_use_id -&amp;gt; (ts, is_error)
&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;btype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;btype&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;inp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;caller&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;btype&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;rid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point is filtering on &lt;code&gt;name == "Agent"&lt;/code&gt;. In Claude Code transcripts, the Task tool is also recorded as &lt;code&gt;name: "Agent"&lt;/code&gt;. Only entries with &lt;code&gt;subagent_type&lt;/code&gt; in &lt;code&gt;input&lt;/code&gt; are what we want — this distinguishes them from plain &lt;code&gt;claude&lt;/code&gt; calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deduplication: skip already-recorded entries by session_id × tool_use_id
&lt;/h3&gt;

&lt;p&gt;A Stop hook can fire multiple times within the same session (&lt;code&gt;/clear&lt;/code&gt;, long sessions). To record without duplicates, the script sweeps the existing log before writing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;seen_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# 同一セッション内の同一 tool_use_id だけをスキップ
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;seen_ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combination of &lt;code&gt;session_id&lt;/code&gt; and &lt;code&gt;tool_use_id&lt;/code&gt; is the uniqueness key. If you deduplicate on &lt;code&gt;tool_use_id&lt;/code&gt; alone, records get silently dropped when IDs happen to collide across different sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Computing duration_ms
&lt;/h3&gt;

&lt;p&gt;The difference between the timestamp of the &lt;code&gt;tool_use&lt;/code&gt; record and the timestamp of its matching &lt;code&gt;tool_result&lt;/code&gt; is the agent's execution time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;use_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;duration_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;tool_result&lt;/code&gt; hasn't arrived yet (e.g. the session was interrupted), the record is written with &lt;code&gt;status: "pending"&lt;/code&gt; and &lt;code&gt;duration_ms: null&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output record format
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-28T16:27:41.766Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sess_xxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_use_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"toolu_014MMSdJubC215oLCxfcrjok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subagent_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"general-purpose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"launchd + cron 総監査"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;177&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"caller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; field is clipped at 300 characters. In the transcript it's free-form text, so it occasionally gets enormous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregating with &lt;code&gt;agent-usage-summary.sh&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The accumulated JSONL is aggregated by &lt;code&gt;~/.claude/scripts/agent-usage-summary.sh&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-usage-summary.sh           &lt;span class="c"&gt;# デフォルト 7d&lt;/span&gt;
agent-usage-summary.sh 30d       &lt;span class="c"&gt;# 30日&lt;/span&gt;
agent-usage-summary.sh 7d 30d    &lt;span class="c"&gt;# 両ウィンドウ同時&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it produces this (actual numbers from today).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== Agent usage (last 7d) ===
total invocations: 86  unique types: 4

Top 10:
  agent                                     calls  errors
  general-purpose                              58       0
  Explore                                      24       0
  fork                                          2       0
  reviewer                                      2       0

0-call agents (defined locally but not used in 7d): 47
  - INDEX
  - a11y-architect
  - architect
  - build-error-resolver
  - code-architect
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;general-purpose&lt;/code&gt; at 58 calls, &lt;code&gt;Explore&lt;/code&gt; at 24.&lt;/strong&gt; These two types account for 95% of all invocations over 7 days. And &lt;strong&gt;47 agents were never called at all&lt;/strong&gt;. This is the input data for zombie-agent pruning.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;~/.claude/scripts/dashboard.sh&lt;/code&gt; folds this output into &lt;code&gt;dashboard.md&lt;/code&gt; every day for always-on visibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"## 🤖 Agent 呼び出し (7d)"&lt;/span&gt;
&lt;span class="nv"&gt;AGENT_OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;~/.claude/scripts/agent-usage-summary.sh 7d 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TOP_BLOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGENT_OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'
  /^Top 10:/ { in_block=1; next }
  /^$/ &amp;amp;&amp;amp; in_block { exit }
  in_block { print }
'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOP_BLOCK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Agents defined as &lt;code&gt;*.md&lt;/code&gt; files under &lt;code&gt;~/.claude/agents/&lt;/code&gt; are treated as "known agents," and any that never appear in the log are listed as "0-call." Non-agent files like &lt;code&gt;INDEX.md&lt;/code&gt; slip in too, so in practice &lt;code&gt;INDEX&lt;/code&gt; shows up at the top of the 0-call list every time. If that bothers you, filter it out with something like &lt;code&gt;.startswith("INDEX")&lt;/code&gt; before building &lt;code&gt;known_agents&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Dashboard integration in &lt;code&gt;dashboard.sh&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dashboard.sh&lt;/code&gt; writes the agent summary out to &lt;code&gt;~/.claude/dashboard.md&lt;/code&gt; alongside health status, auto-skills counts, and the launchd job list. Updated daily via cron, it lets you check every morning which agents are pulling their weight this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Stop hook's stdin can only be read once&lt;/strong&gt; → &lt;code&gt;stop_hooks_combined.sh&lt;/code&gt; was designed to write a tmpfile first and then redirect it into each hook. My first attempt had each hook &lt;code&gt;cat&lt;/code&gt; stdin directly, and every hook after the first got nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mktemp&lt;/code&gt; fails when TMPDIR is broken&lt;/strong&gt; → added a fallback in &lt;code&gt;stop_hooks_combined.sh&lt;/code&gt;: &lt;code&gt;|| PAYLOAD="/tmp/stop-hook.$$.$RANDOM.json"&lt;/code&gt;. If you redirect to an empty path, every hook silently becomes a no-op.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicating on tool_use_id alone loses records on cross-session collisions&lt;/strong&gt; → fixed by keying on the &lt;code&gt;session_id × tool_use_id&lt;/code&gt; pair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Searching for &lt;code&gt;name == "Task"&lt;/code&gt; returns nothing&lt;/strong&gt; → in Claude Code transcripts, the Task tool is recorded as &lt;code&gt;name: "Agent"&lt;/code&gt;. This isn't documented anywhere; I found it by grepping the actual files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;description can grow to thousands of characters&lt;/strong&gt; → without clipping at 300 characters, the JSONL bloated and &lt;code&gt;json.loads&lt;/code&gt; got slow in later aggregation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Stop hook can fire before the &lt;code&gt;tool_result&lt;/code&gt; arrives&lt;/strong&gt; → e.g. when a session is force-killed. Recording it with &lt;code&gt;status: "pending"&lt;/code&gt; and &lt;code&gt;duration_ms: null&lt;/code&gt; lets aggregation queries exclude it with an &lt;code&gt;is not null&lt;/code&gt; filter.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Stop hook payload includes &lt;code&gt;transcript_path&lt;/code&gt;, which means &lt;strong&gt;the entire conversation's tool call history is readable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Lines with &lt;code&gt;name == "Agent"&lt;/code&gt; and an &lt;code&gt;input.subagent_type&lt;/code&gt; are agent invocations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicate on &lt;code&gt;session_id × tool_use_id&lt;/code&gt;&lt;/strong&gt; to handle multiple firings within the same session&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;duration_ms&lt;/code&gt; is the timestamp delta between &lt;code&gt;tool_use&lt;/code&gt; and &lt;code&gt;tool_result&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;0-call section of &lt;code&gt;agent-usage-summary.sh&lt;/code&gt; is a detector for "agents you defined but never use"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: the mechanism that automatically retires those unused agents surfaced by this aggregation — the design behind &lt;a href="https://dev.to/bokuwalily/hunting-down-zombie-agents-logging-subagent-usage-with-a-claude-code-stop-hook-332g"&gt;automatically pruning zombie agents&lt;/a&gt; (already published).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>shell</category>
      <category>python</category>
    </item>
    <item>
      <title>Hunting Down Zombie Agents: Logging Subagent Usage with a Claude Code Stop Hook</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Thu, 23 Jul 2026 00:00:06 +0000</pubDate>
      <link>https://dev.to/bokuwalily/hunting-down-zombie-agents-logging-subagent-usage-with-a-claude-code-stop-hook-332g</link>
      <guid>https://dev.to/bokuwalily/hunting-down-zombie-agents-logging-subagent-usage-with-a-claude-code-stop-hook-332g</guid>
      <description>&lt;p&gt;Quick question: how many of the subagents you've defined in &lt;code&gt;~/.claude/agents/&lt;/code&gt; have actually been called this month? I couldn't have told you — until I started measuring, and this morning's tally revealed &lt;strong&gt;44 agents that hadn't been invoked even once in 30 days&lt;/strong&gt;. This post is part of my "Claude Code environment" series (a follow-up to &lt;a href="https://dev.to/bokuwalily/snap-8-windows-into-a-grid-on-an-ultrawide-in-one-command-a-swift-applescript-convergence-loop-4oha"&gt;automatic terminal window tiling&lt;/a&gt;), continuing the theme of keeping the setup from bloating. I'll walk through, with real code, a system that records subagent invocations to JSONL via a Stop hook and uses a daily report to flush out the zombies and cull them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: the gap between "defined" and "used"
&lt;/h2&gt;

&lt;p&gt;Adding an agent to Claude Code is as simple as writing frontmatter in &lt;code&gt;.claude/agents/*.md&lt;/code&gt;. Because it's so easy, "let's just define it for now" agents pile up. The problem is that there's no way to check whether an agent is actually being used.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's no mechanism to aggregate invocation counts across sessions&lt;/li&gt;
&lt;li&gt;A hand-written &lt;code&gt;INDEX.md&lt;/code&gt; goes stale immediately&lt;/li&gt;
&lt;li&gt;There's no evidence to base a "can I delete this?" decision on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An agent with zero usage still takes up space in the context injected at startup. Left alone, it becomes a zombie: defined but unused → unclear whether it even works → can't be deleted either.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overall flow
&lt;/h2&gt;

&lt;p&gt;The system consists of three components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;セッション終了
  └─ Stop hook (stop_agent_tracker.sh)
       └─ transcript.jsonl を解析 → agent-invocations.jsonl に追記
                                              │
                     毎日 10:15 launchd ──────┘
                       └─ agent-usage-summary.sh 7d 30d
                            └─ agent-usage-latest.md（Top10 + 0回リスト）

  手動 or cron
   └─ agents-index.sh → INDEX.md 自動再生成
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Record to JSONL with a Stop hook
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/.claude/hooks/stop_agent_tracker.sh&lt;/code&gt; runs when a session ends. The Stop hook receives session info and a &lt;code&gt;transcript_path&lt;/code&gt; on stdin, so the script parses that transcript and picks out Agent tool invocations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# stop_agent_tracker.sh（抜粋）&lt;/span&gt;
&lt;span class="c"&gt;# stdin: {"session_id":"...","transcript_path":"...","hook_event_name":"Stop",...}&lt;/span&gt;
&lt;span class="c"&gt;# 出力:  ~/.claude/logs/agent-invocations.jsonl&lt;/span&gt;

&lt;span class="nv"&gt;OUT_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_DIR&lt;/span&gt;&lt;span class="s2"&gt;/agent-invocations.jsonl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The heart of the Python portion is a two-pass parse of the transcript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 第1パス: tool_use (name="Agent") と tool_result をインデックス化
&lt;/span&gt;&lt;span class="n"&gt;uses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;    &lt;span class="c1"&gt;# id -&amp;gt; (ts, name, input, caller)
&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;# tool_use_id -&amp;gt; (ts, is_error)
&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;btype&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;inp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;caller&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;btype&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In Claude Code transcripts, the "Task" tool is recorded as &lt;code&gt;name="Agent"&lt;/code&gt;. The &lt;code&gt;subagent_type&lt;/code&gt; lives in &lt;code&gt;input.subagent_type&lt;/code&gt;. The same comment appears in the code itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To prevent duplicates, the same &lt;code&gt;session_id&lt;/code&gt; + &lt;code&gt;tool_use_id&lt;/code&gt; combination is skipped (so even if the Stop hook fires multiple times in one session, nothing gets written twice).&lt;/p&gt;

&lt;p&gt;A single JSONL record looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"ts": "2026-05-28T16:27:41.766Z", "session_id": "TEST-AGENT-TRACKER-001",
 "cwd": "~", "tool_use_id": "toolu_014MM...", "subagent_type": "general-purpose",
 "description": "launchd + cron 総監査", "duration_ms": 177, "status": "ok",
 "caller": {"type": "direct"}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far, 546 records (about 176 KB) have accumulated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Produce a Top 10 and zero-call list over 7d/30d windows
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/.claude/scripts/agent-usage-summary.sh&lt;/code&gt; handles the aggregation. It's inline python3 with no external libraries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 使い方&lt;/span&gt;
agent-usage-summary.sh           &lt;span class="c"&gt;# デフォルト 7d&lt;/span&gt;
agent-usage-summary.sh 30d       &lt;span class="c"&gt;# 30日&lt;/span&gt;
agent-usage-summary.sh 7d 30d    &lt;span class="c"&gt;# 両方（launchdはこれ）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internally it's three steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ① JOSNLをロードしてウィンドウでフィルタ
&lt;/span&gt;&lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;td&lt;/span&gt;
&lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_dt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# ② subagent_type でカウント + エラー数
&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ③ ~/.claude/agents/*.md のファイル名一覧と突き合わせて未使用を出す
&lt;/span&gt;&lt;span class="n"&gt;known_agents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agents_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;known_agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;unused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;known_agents&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's this morning's actual output (&lt;code&gt;~/.claude/logs/agent-usage-latest.md&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== Agent usage (last 7d) ===
total invocations: 127  unique types: 5

Top 10:
  agent                                     calls  errors
  general-purpose                              76       0
  Explore                                      45       0
  Content Creator                               2       0
  fork                                          2       0
  reviewer                                      2       0

0-call agents (defined locally but not used in 7d): 47
  - a11y-architect
  - architect
  - build-error-resolver
  - code-architect
  - code-explorer
  - code-reviewer
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 7 days, only 5 agent types were called and 47 weren't called at all. Even in the 30-day window, 44 remain at zero. &lt;code&gt;general-purpose&lt;/code&gt; and &lt;code&gt;Explore&lt;/code&gt; account for 95% of all invocations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Auto-regenerate INDEX.md with agents-index.sh
&lt;/h2&gt;

&lt;p&gt;To decide whether something can be deleted, you need a cross-cutting view of what each agent was written for. &lt;code&gt;agents-index.sh&lt;/code&gt; reads the frontmatter of &lt;code&gt;~/.claude/agents/*.md&lt;/code&gt; and builds &lt;code&gt;INDEX.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 簡易frontmatterパーサ（PyYAML依存なし）
&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^---\n(.*?)\n---\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated &lt;code&gt;INDEX.md&lt;/code&gt; is a table like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- AUTO-GENERATED by ~/.claude/scripts/agents-index.sh — DO NOT EDIT MANUALLY --&amp;gt;&lt;/span&gt;
&lt;span class="gh"&gt;# Agents Index (51 agents · 2026-07-14 10:15)&lt;/span&gt;

| Name | Model | Description | Tools |
|------|-------|-------------|-------|
| &lt;span class="sb"&gt;`general-purpose`&lt;/span&gt; (general-purpose.md) | - | General-purpose agent for... | &lt;span class="err"&gt;*&lt;/span&gt; |
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the &lt;code&gt;--json&lt;/code&gt; flag it also writes out &lt;code&gt;.index.json&lt;/code&gt;, which can be reused programmatically by things like a cost tracker.&lt;/p&gt;

&lt;p&gt;Files missing &lt;code&gt;name&lt;/code&gt; or &lt;code&gt;description&lt;/code&gt; in their frontmatter get listed under a &lt;code&gt;⚠️ Validation warnings&lt;/code&gt; section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Generate the daily report with launchd
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/Library/LaunchAgents/com.shun.agent-usage-daily.plist&lt;/code&gt; runs every day at 10:15.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;15&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command it runs is simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/bin/zsh -c
  ~/.claude/scripts/agent-usage-summary.sh 7d 30d
  &amp;gt; ~/.claude/logs/agent-usage-latest.md 2&amp;gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PATH explicitly includes nvm and Homebrew because launchd's default PATH won't find &lt;code&gt;python3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With this in place, &lt;code&gt;agent-usage-latest.md&lt;/code&gt; gets refreshed every morning at 10:15, so I always know how many agents have sat at zero for 30 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operating routine: cull the zombies
&lt;/h2&gt;

&lt;p&gt;Once a week, I run these commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# INDEX再生成（frontmatter変更・追加後に必ず走らせる）&lt;/span&gt;
~/.claude/scripts/agents-index.sh

&lt;span class="c"&gt;# 0回リスト確認&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.claude/logs/agent-usage-latest.md | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 9999 &lt;span class="s2"&gt;"0-call agents"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My decision criteria look like this.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 calls in 30d, and reading the description reveals no realistic use case&lt;/td&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0 calls in 30d, but there's a future use case&lt;/td&gt;
&lt;td&gt;Keep, and spell out the usage conditions in the description&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0 calls in 7d, a few calls in 30d&lt;/td&gt;
&lt;td&gt;Possibly a seasonal job — hold off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top 5 regular&lt;/td&gt;
&lt;td&gt;Revisit its permissions and tool definitions to sharpen it further&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this round of housekeeping I deleted 14 agents confirmed at zero calls over 30 days, including &lt;code&gt;a11y-architect&lt;/code&gt;, &lt;code&gt;django-reviewer&lt;/code&gt;, and &lt;code&gt;go-build-resolver&lt;/code&gt;. They simply disappear from INDEX.md — nothing else changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filtering on &lt;code&gt;name="Task"&lt;/code&gt; matched nothing&lt;/strong&gt; → In Claude Code transcripts, Task invocations are recorded as &lt;code&gt;name="Agent"&lt;/code&gt;. At first every record came back empty because of this&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Stop hook fired multiple times in one session, causing double writes&lt;/strong&gt; → Fixed by adding a &lt;code&gt;seen_ids&lt;/code&gt; duplicate check on &lt;code&gt;(session_id, tool_use_id)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Records with an empty-string &lt;code&gt;subagent_type&lt;/code&gt; crept in&lt;/strong&gt; → Filter with &lt;code&gt;if r.get("subagent_type")&lt;/code&gt; (the empty string being falsy is enough)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;agents-index.sh parsed &lt;code&gt;INDEX.md&lt;/code&gt; itself, creating a loop&lt;/strong&gt; → Excluded with &lt;code&gt;if p.name == "INDEX.md": continue&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;launchd's minimal PATH couldn't find &lt;code&gt;python3&lt;/code&gt;&lt;/strong&gt; → Explicitly set the nvm and Homebrew paths in the plist's EnvironmentVariables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents at zero in the 30d window looked "nonexistent" in the 7d window&lt;/strong&gt; → &lt;code&gt;known_agents&lt;/code&gt; is built from the file listing of &lt;code&gt;agents_dir&lt;/code&gt;, so it doesn't depend on the window. This is the correct behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Stop hook appends to JSONL keyed by session_id + tool_use_id&lt;/strong&gt; → invocation history accumulates across sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;agent-usage-summary.sh&lt;/strong&gt; outputs a Top 10 and a zero-call agent list over 7d/30d windows. Since it cross-references the filenames in &lt;code&gt;~/.claude/agents/*.md&lt;/code&gt;, any newly added agent is automatically tracked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;agents-index.sh&lt;/strong&gt; auto-regenerates INDEX.md from frontmatter. A hand-written index goes stale immediately, so this replaces it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;launchd&lt;/strong&gt; runs the aggregation daily at 10:15 and writes to &lt;code&gt;agent-usage-latest.md&lt;/code&gt;, so "how many zombies do I have as of today?" is always answerable&lt;/li&gt;
&lt;li&gt;Agents confirmed at zero calls over 30 days get deleted. When the case for deletion comes from numbers, there's no second-guessing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I'll use this log data to visualize which agents get combined for which kinds of work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Snap 8 Windows into a Grid on an Ultrawide in One Command — a Swift + AppleScript Convergence Loop and 5 Traps</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Wed, 22 Jul 2026 11:00:48 +0000</pubDate>
      <link>https://dev.to/bokuwalily/snap-8-windows-into-a-grid-on-an-ultrawide-in-one-command-a-swift-applescript-convergence-loop-4oha</link>
      <guid>https://dev.to/bokuwalily/snap-8-windows-into-a-grid-on-an-ultrawide-in-one-command-a-swift-applescript-convergence-loop-4oha</guid>
      <description>&lt;p&gt;This is part of my "Claude Code environment" series. Last time, in &lt;a href="https://dev.to/bokuwalily/your-claude-code-setup-gets-bloated-if-you-ignore-it-weekly-auto-slimming-by-watching-injected-3jki"&gt;The Environment-Degradation Sentinel&lt;/a&gt;, I wrote about self-diagnosing a Claude Code environment. This time it's a different kind of workflow cost.&lt;/p&gt;

&lt;p&gt;Picture eight Terminal.app windows scattered across an ultrawide monitor. A single command snaps them into a 4×2 grid. It looks like "just line up windows with osascript," but there are traps across three layers: &lt;strong&gt;coordinate-system conversion, AppleScript's asynchronous behavior, and macOS's row rounding.&lt;/strong&gt; In this article I'll explain the two design cores and the five traps I hit during implementation, with real code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: dragging windows into place cost me a minute
&lt;/h2&gt;

&lt;p&gt;When I line up multiple Claude Code sessions on a 3440×1440 ultrawide, the windows open in random positions every time. Fullscreen split isn't enough. Mission Control can't pin positions. In the end I was spending a minute every morning dragging all eight windows by mouse into a 4×2 arrangement.&lt;/p&gt;

&lt;p&gt;Writing the &lt;code&gt;position&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt; directly in AppleScript should have been the end of it. But once I actually started writing, three things became clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;There are two coordinate systems&lt;/strong&gt; (Cocoa's Y-up and screen coordinates' Y-down)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;set position&lt;/code&gt; gets silently ignored&lt;/strong&gt; in some situations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The window's height doesn't end up at the value I specified&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll walk through the design of a script that solves all of these together (&lt;code&gt;~/.claude/scripts/terminal-grid8.sh&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Design core ①: Convert visibleFrame to global coordinates in Swift and hand it to the shell
&lt;/h2&gt;

&lt;p&gt;AppleScript's &lt;code&gt;position of window&lt;/code&gt; returns global coordinates (top-left origin, Y-down). Meanwhile macOS's &lt;code&gt;NSScreen.visibleFrame&lt;/code&gt; uses the Cocoa coordinate system (Y-up). Mixing the two makes the math go wrong. I do the conversion all at once in Swift and hand it off via the shell variables &lt;code&gt;GX GY GW GH&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GEOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;swift - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
import AppKit
let screens = NSScreen.screens
guard let main = screens.first else { exit(1) }
// 最も横長の画面を対象にする(ウルトラワイド)。同率ならメイン以外を優先
let target = screens.max(by: { &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="sh"&gt;.frame.width &amp;lt; &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;.frame.width })!
let v = target.visibleFrame   // Dock除外(Cocoa座標: 左下原点Y上向き)
let mainH = main.frame.height
var topY = mainH - (v.origin.y + v.height)  // グローバル(左上原点)座標へ変換
var h = v.height
// visibleFrameがメニューバーを反映しない画面がある → 手動で差し引く
if v.height == target.frame.height {
    topY += 30
    h -= 30
}
print("&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sh"&gt;Int(v.origin.x)) &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sh"&gt;Int(topY)) &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sh"&gt;Int(v.width)) &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sh"&gt;Int(h))")
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; GX GY GW GH &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GEOM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is the conversion formula &lt;code&gt;mainH - (v.origin.y + v.height)&lt;/code&gt;. Cocoa's &lt;code&gt;visibleFrame.origin.y&lt;/code&gt; is the distance from the bottom edge of the main screen (Y-up), so to turn it into a global "distance from the top-left (Y-down)," you subtract from the height of the main screen.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;if v.height == target.frame.height&lt;/code&gt; branch is a fallback that emerged from real measurement. On non-main screens, &lt;code&gt;visibleFrame&lt;/code&gt; can return the same value as the frame without subtracting the 30px menu bar. In that case I subtract it manually on the Swift side (trap ④, below).&lt;/p&gt;

&lt;p&gt;The cell dimensions are a simple integer division.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; GW &lt;span class="o"&gt;/&lt;/span&gt; COLS &lt;span class="k"&gt;))&lt;/span&gt;   &lt;span class="c"&gt;# 列幅&lt;/span&gt;
&lt;span class="nv"&gt;CH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; GH &lt;span class="o"&gt;/&lt;/span&gt; ROWS &lt;span class="k"&gt;))&lt;/span&gt;   &lt;span class="c"&gt;# 行高&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Design core ②: AppleScript's convergence loop
&lt;/h2&gt;

&lt;p&gt;The backbone of the convergence loop is that it &lt;strong&gt;re-scans indexes on every pass&lt;/strong&gt; — not by name, not by ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 収束ループ: 毎パス再スキャンして未配置の窓を1枚だけ動かす&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;nCells&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;occupied&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nCells&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;times&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;occupied&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;windows&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;-- 1) 対象画面上の窓を走査し、セル一致済みをマーク・最初の未配置窓を記録&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nb"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;99999&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;99999&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;≥&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;gx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;tol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;gx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nCols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;≥&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;gy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;tol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;gy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nRows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="c1"&gt;-- セル一致判定 ...（tol=40px の誤差許容）&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;-- 2) 未配置窓が無ければ収束&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;-- 3) 最初の空きセルへ移動 (size→delay→position→delay→size、delay 必須)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;cw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ty&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;cw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;-- 読み返して position を最終補正(リサイズでのドリフト対策)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;is not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;tx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;is not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;moverIdx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ty&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two reasons I designed this as "move exactly one window and re-scan as long as any remain unplaced," rather than "move all the windows at once with a forEach":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name references don't work&lt;/strong&gt; (trap ①), so you can't hold an object reference across passes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moving one window changes the cells' occupancy state&lt;/strong&gt;, so you have to redo the scan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even in the worst case (all 8 windows are out of place), &lt;code&gt;nCells * 3 = 24&lt;/code&gt; passes are enough to converge.&lt;/p&gt;

&lt;p&gt;:::message&lt;br&gt;
If you drop &lt;code&gt;delay 0.2&lt;/code&gt;, &lt;code&gt;set position&lt;/code&gt; is silently ignored (trap ②). AppleScript doesn't throw an error either. You need the five steps &lt;code&gt;size → delay → position → delay → size&lt;/code&gt;, plus the fallback that reads back and re-corrects.&lt;br&gt;
:::&lt;/p&gt;

&lt;p&gt;After the convergence loop, there's a normalization pass that unifies the bottom row's y by actual height.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 最下段yの正規化: 行丸めでセル高を超える実高のクランプ揺れ対策&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bottomEdge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;gy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nRows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;lastRowTop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;gy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;nRows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;-- 最下段の窓だけを対象に実高で下端揃え&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;≥&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;lastRowTop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;tol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bottomEdge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;wh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;-- 実際のピクセル高&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;targetY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bottomEdge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;wh&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;is not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;targetY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;bestX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;targetY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The traps I hit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ① Window name references break down while the spinner is rotating
&lt;/h3&gt;

&lt;p&gt;Terminal.app replaces the title with spinner characters (⠐⠂✳) while a tab is loading. If you reference a window by name right after a session starts, you get &lt;code&gt;-1728&lt;/code&gt; (object not found). Stop referencing by name; operate directly by the &lt;code&gt;window i&lt;/code&gt; index on every pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  ② A &lt;code&gt;position&lt;/code&gt; set right after &lt;code&gt;size&lt;/code&gt; is ignored without a delay
&lt;/h3&gt;

&lt;p&gt;If you put &lt;code&gt;set position of window i to {tx, ty}&lt;/code&gt; right after a resize, the coordinate change arrives before macOS's window server has finished the previous resize, and it gets dropped. Put &lt;code&gt;delay 0.2&lt;/code&gt; in two places (after &lt;code&gt;size&lt;/code&gt; and after &lt;code&gt;position&lt;/code&gt;), then read back and correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  ③ Terminal's row-height rounding keeps the cell height off the specified value
&lt;/h3&gt;

&lt;p&gt;Terminal.app rounds a window's height to "rows × row height." For example, even if you specify a cell height of 705px, the actual value becomes 44 rows × 16px = 704px, or 45 rows × 16px = 720px. This error, combined with macOS's boundary clamping, shifts the bottom row's windows by a few px and makes the cell-match check fail. Run a "bottom-edge alignment by actual height" normalization pass after the convergence loop to absorb it.&lt;/p&gt;

&lt;h3&gt;
  
  
  ④ visibleFrame sometimes doesn't subtract the menu bar
&lt;/h3&gt;

&lt;p&gt;On non-main screens, &lt;code&gt;NSScreen.visibleFrame&lt;/code&gt; can return the same value as &lt;code&gt;NSScreen.frame&lt;/code&gt; (observed in practice). In that case the computed top edge of the grid overlaps the menu bar. On the Swift side, apply &lt;code&gt;topY += 30 / h -= 30&lt;/code&gt; manually, conditioned on &lt;code&gt;v.height == target.frame.height&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;topY&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ⑤ Transient errors (-10006 / -1728) recover naturally on retry
&lt;/h3&gt;

&lt;p&gt;While a window is initializing or its name is changing, you get &lt;code&gt;-10006&lt;/code&gt; (invalid index) or &lt;code&gt;-1728&lt;/code&gt;. If you insert &lt;code&gt;delay 0.3&lt;/code&gt; in an &lt;code&gt;on error&lt;/code&gt; and fall through to the next pass, the convergence loop re-scans that same window on the following pass and picks it up. The key is not to abort immediately on an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 過渡エラーは次パスで再試行&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage and verification
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.claude/scripts/terminal-grid8.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command aligns Terminal.app's 8 windows. You can change the number of divisions with just the two lines at the top.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;COLS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4
&lt;span class="nv"&gt;ROWS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want 6 divisions, rewrite it to &lt;code&gt;COLS=3 ROWS=2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The final report prints each window's &lt;code&gt;x,y WxH title&lt;/code&gt;. On a 3440-wide screen, success means x converges to the four values &lt;code&gt;0/860/1720/2580&lt;/code&gt;, and y converges to one value each for the top and bottom rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cocoa Y-up → global Y-down conversion&lt;/strong&gt; is handled by the single Swift line &lt;code&gt;mainH - (v.origin.y + v.height)&lt;/code&gt; and handed to the shell&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AppleScript's convergence loop&lt;/strong&gt; repeats "move one window and re-scan" instead of a foreach. Drop name references and operate by index on every pass&lt;/li&gt;
&lt;li&gt;If you omit &lt;code&gt;delay 0.2&lt;/code&gt;, &lt;code&gt;set position&lt;/code&gt; is silently ignored&lt;/li&gt;
&lt;li&gt;The y drift from Terminal's row-height rounding is normalized by a post-convergence "bottom-edge alignment by actual height" pass&lt;/li&gt;
&lt;li&gt;Because some screens don't reflect the menu bar in &lt;code&gt;visibleFrame&lt;/code&gt;, add a manual 30px correction conditioned on &lt;code&gt;height == frame.height&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I'll write about an &lt;strong&gt;environment-alignment hook that fires on Claude Code startup&lt;/strong&gt; using this script as a trigger — placing Terminal windows, the status line, and Obsidian into fixed positions all at once from launchd.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>applescript</category>
      <category>macos</category>
      <category>swift</category>
      <category>automation</category>
    </item>
    <item>
      <title>Your Claude Code Setup Gets Bloated If You Ignore It — Weekly Auto-Slimming by Watching Injected Bytes, Agent Count, and Frustration Words</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Wed, 22 Jul 2026 05:00:07 +0000</pubDate>
      <link>https://dev.to/bokuwalily/your-claude-code-setup-gets-bloated-if-you-ignore-it-weekly-auto-slimming-by-watching-injected-3jki</link>
      <guid>https://dev.to/bokuwalily/your-claude-code-setup-gets-bloated-if-you-ignore-it-weekly-auto-slimming-by-watching-injected-3jki</guid>
      <description>&lt;p&gt;This is the next installment in my "Claude Code environment" series. Combined with the &lt;a href="https://dev.to/bokuwalily/rescuing-a-long-running-job-with-sub-step-idempotency-how-vault-auto-ingest-escaped-timeout-hell-521d"&gt;idempotent marker implementation&lt;/a&gt; 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: &lt;strong&gt;environment decay&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A Claude Code setup &lt;strong&gt;gets fat if you leave it alone&lt;/strong&gt;. 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, &lt;strong&gt;lets &lt;code&gt;claude -p&lt;/code&gt; fix it, and then confirms the improvement with an independent re-measurement after the fix&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: 99 "retired" agents were still being injected
&lt;/h2&gt;

&lt;p&gt;This is a real incident I hit during a performance audit on 2026-07-11.&lt;/p&gt;

&lt;p&gt;I'd tidied up &lt;code&gt;~/.claude/agents/&lt;/code&gt; several times. I'd manually moved agent definitions I no longer used into &lt;code&gt;agents-archive/&lt;/code&gt; and figured I was done. But when I actually measured, &lt;code&gt;agents_loaded&lt;/code&gt; was still stuck at 99. The culprit was dot-directories (&lt;code&gt;.deprecated/&lt;/code&gt;, &lt;code&gt;.backup/&lt;/code&gt;, and the like). &lt;code&gt;find&lt;/code&gt;'s default behavior recurses into dot-directories too. &lt;strong&gt;They looked deleted, but every single one was still being loaded through the hidden directories.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This real incident is what prompted me to write &lt;code&gt;cc-self-audit.sh&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five metrics
&lt;/h2&gt;

&lt;p&gt;The script's &lt;code&gt;collect()&lt;/code&gt; function grabs five metrics every run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.claude/scripts/cc-self-audit.sh（collect関数より）&lt;/span&gt;

&lt;span class="c"&gt;# --- 静的 ---&lt;/span&gt;
&lt;span class="nv"&gt;inject_bytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/rules"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; 2&amp;gt;/dev/null | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/CLAUDE.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/CLAUDE.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/projects/-Users-matsubara/memory/MEMORY.md"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;agents_loaded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/agents"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# --- 動的（前回実行以降のtranscriptのみ・最大200ファイル）---&lt;/span&gt;
&lt;span class="nv"&gt;stopspam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | xargs /usr/bin/grep &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'セルフ監査未実施。実装'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{s+=$1} END{print s+0}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;frustration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | xargs /usr/bin/grep &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'何回も言|いい加減にし|嘘つ|舐めんな|なんで治らん|最悪やろ|頭悪い'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{s+=$1} END{print s+0}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;toolerr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | xargs /usr/bin/grep &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'"is_error":true'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{s+=$1} END{print s+0}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inject_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Total bytes of rules + CLAUDE.md + MEMORY.md&lt;/td&gt;
&lt;td&gt;40,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agents_loaded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Total .md count under &lt;code&gt;~/.claude/agents/&lt;/code&gt; (recursion including dot-dirs)&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stopspam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of times the audit hook fired / week&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frustration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Occurrences of frustration words in transcripts / week&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;toolerr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Count of &lt;code&gt;"is_error":true&lt;/code&gt; in transcripts / week&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two static metrics are proxy variables for &lt;strong&gt;injection cost&lt;/strong&gt;, and the three dynamic metrics are proxy variables for &lt;strong&gt;experience quality&lt;/strong&gt;. 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).&lt;/p&gt;

&lt;p&gt;The thresholds can be overridden with env variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TH_INJECT_BYTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_INJECT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;40000&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TH_AGENTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_AGENTS&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;60&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TH_STOPSPAM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_STOPSPAM&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;15&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TH_FRUSTRATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_FRUST&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;8&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TH_TOOLERR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SELF_AUDIT_TH_TOOLERR&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;400&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When a threshold is exceeded, &lt;code&gt;claude -p&lt;/code&gt; fixes itself
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BREACH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$METRICS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | breaches&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BREACH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;log &lt;span class="s2"&gt;"GREEN"&lt;/span&gt;
  &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LASTRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  notify &lt;span class="s2"&gt;"✅ CC自己監査: 正常 (&lt;/span&gt;&lt;span class="nv"&gt;$METRICS&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;log &lt;span class="s2"&gt;"RED: &lt;/span&gt;&lt;span class="nv"&gt;$BREACH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;log &lt;span class="s2"&gt;"DRY=1: 修正スキップ"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LASTRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt passed to &lt;code&gt;claude -p&lt;/code&gt; has a remediation policy embedded for each decay pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROMPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"...
## 指示
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件ずつ &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHANGELOG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; に「日時/対象/理由/戻し方」を追記
4. 修正後に必ず 'bash ~/.claude/scripts/cc-self-audit.sh --collect-only' を実行し改善を数値で確認
..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
Having claude run &lt;code&gt;--collect-only&lt;/code&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A "don't trust self-reporting" fail-closed design
&lt;/h2&gt;

&lt;p&gt;Even when &lt;code&gt;claude -p&lt;/code&gt; says "fixed it," I don't believe it right away. The outer harness &lt;strong&gt;calls &lt;code&gt;collect()&lt;/code&gt; again to re-measure the numbers&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 独立再計測（自己申告は信じない）&lt;/span&gt;
&lt;span class="nv"&gt;AFTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;collect&lt;span class="si"&gt;)&lt;/span&gt;
log &lt;span class="s2"&gt;"after: &lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HISTORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;STILL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AFTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | breaches&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LASTRUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STILL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;notify &lt;span class="s2"&gt;"🔧 CC自己監査: 劣化検知→自己修正済み。前:[&lt;/span&gt;&lt;span class="nv"&gt;$BREACH&lt;/span&gt;&lt;span class="s2"&gt;] 後:全緑。詳細=&lt;/span&gt;&lt;span class="nv"&gt;$CHANGELOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;notify &lt;span class="s2"&gt;"🚨 CC自己監査: 自己修正後も残存 [&lt;/span&gt;&lt;span class="nv"&gt;$STILL&lt;/span&gt;&lt;span class="s2"&gt;]。要確認: &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt; / &lt;/span&gt;&lt;span class="nv"&gt;$CHANGELOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;All measured values are stacked one record per line in &lt;code&gt;~/.claude/self-audit/history.jsonl&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weekly launch via launchd, Sundays at 8:30
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ~/Library/LaunchAgents/com.lily.cc-self-audit.plist --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Weekday&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;8&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;30&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;false/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;There are two modes for manual runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 計測だけ（claudeを呼ばない）&lt;/span&gt;
~/.claude/scripts/cc-self-audit.sh &lt;span class="nt"&gt;--collect-only&lt;/span&gt;

&lt;span class="c"&gt;# ドライラン（違反表示のみ・修正なし）&lt;/span&gt;
&lt;span class="nv"&gt;SELF_AUDIT_DRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ~/.claude/scripts/cc-self-audit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before putting it into production, I always eyeballed the metrics with &lt;code&gt;DRY=1&lt;/code&gt; first, then enabled it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;&lt;br&gt;
As the comment says — &lt;code&gt;bash 3.2 compatible, fail-open&lt;/code&gt; — the audit script itself is designed so that nothing breaks even if it crashes. It uses &lt;code&gt;set -uo pipefail&lt;/code&gt;, each individual measurement swallows errors with &lt;code&gt;2&amp;gt;/dev/null || true&lt;/code&gt;, 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Claude Code setup accumulates decay that, if left alone, can be measured across five metrics: &lt;strong&gt;inject_bytes, agent count, hook spam, frustration words, and tool failures&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;When a threshold is exceeded, &lt;code&gt;claude -p&lt;/code&gt; fixes only what's inside &lt;code&gt;~/.claude&lt;/code&gt;, and records it in the change log (&lt;code&gt;self-audit-changes.log&lt;/code&gt;) with a "how to revert" note&lt;/li&gt;
&lt;li&gt;After the fix, an &lt;strong&gt;outer independent re-measurement&lt;/strong&gt; verifies the improvement numerically. A self-reported "fixed it" alone is not treated as done&lt;/li&gt;
&lt;li&gt;Weekly launch via launchd, Sundays at 8:30. You can manually check with &lt;code&gt;--collect-only&lt;/code&gt; and &lt;code&gt;DRY=1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;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)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I'll write about &lt;strong&gt;how I trimmed down the inject_bytes overage&lt;/strong&gt; that this audit loop detected — a pipeline that compresses the rules and auto-slims them every week.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>launchd</category>
      <category>bash</category>
    </item>
  </channel>
</rss>
