<?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: Reporails</title>
    <description>The latest articles on DEV Community by Reporails (reporails).</description>
    <link>https://dev.to/reporails</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%2Forganization%2Fprofile_image%2F12470%2Feb6c7166-21ab-442d-a75f-7ec3c525f1cd.png</url>
      <title>DEV Community: Reporails</title>
      <link>https://dev.to/reporails</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reporails"/>
    <language>en</language>
    <item>
      <title>State of AI Instruction Quality: A Third of Repos Shout</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:37:49 +0000</pubDate>
      <link>https://dev.to/reporails/state-of-ai-instruction-quality-a-third-of-repos-shout-39ei</link>
      <guid>https://dev.to/reporails/state-of-ai-instruction-quality-a-third-of-repos-shout-39ei</guid>
      <description>&lt;p&gt;&lt;code&gt;IMPORTANT&lt;/code&gt;. &lt;code&gt;CRITICAL&lt;/code&gt;. &lt;code&gt;You MUST&lt;/code&gt;. &lt;code&gt;NEVER&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A third of the repos in &lt;a href="https://reporails.com/articles/state-of-ai-instruction-quality-q3-2026" rel="noopener noreferrer"&gt;the Q3 report&lt;/a&gt;, 3,319 of the 9,784 with an agent setup, write at least one word like these in capitals in their instruction files, 36,482 times between them. It's the most common habit we counted, and the rest of the list reads like a prompt-writing history: emoji, "please", "think step by step", a warning about hallucinating.&lt;/p&gt;

&lt;p&gt;Each habit travels with a particular kind of setup, and some with particular findings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp71ngjo7w6sp5kq1k32e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp71ngjo7w6sp5kq1k32e.png" alt="Share of 9,784 repos with each habit in their instruction files: ALL-CAPS emphasis 33.9%, emoji 15.8%, " width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who has them
&lt;/h2&gt;

&lt;p&gt;The habits live in the big setups. The median repo in the report has 2 instruction files. For repos with one of these habits, the median runs from 5 files to 14.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repos with…&lt;/th&gt;
&lt;th&gt;Median instruction files&lt;/th&gt;
&lt;th&gt;With an agent hook&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All repos&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;7.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALL-CAPS&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;13.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Please"&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;13.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emoji&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;15.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A destructive-command rule&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Hallucinate"&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;21.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TODO, TBD, FIXME&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;15.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Step by step"&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;17.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Part of that is arithmetic. More files mean more text, and more chances to contain any given word. They have more of everything else too. Repos with these habits are two to four times as likely to have sub-agents, and more of them write for more than one agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  ALL-CAPS: a third of repos
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MUST&lt;/code&gt; is the most common capitalized word, then &lt;code&gt;CRITICAL&lt;/code&gt;, &lt;code&gt;NEVER&lt;/code&gt; and &lt;code&gt;ALWAYS&lt;/code&gt;. It's least common in &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;AGENTS.md&lt;/code&gt; main files, where about 22% of setups have it, and most common in Gemini, Cursor and Copilot ones, at 31% to 35%.&lt;/p&gt;

&lt;p&gt;Some of it is RFC-style &lt;code&gt;MUST&lt;/code&gt; in a spec, a convention with a defined meaning. Much of it reads like this line from a popular project's Copilot review instructions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If test coverage is sufficient, &lt;strong&gt;DO NOT COMMENT&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Repos that write in capitals are firmer in general. They carry fewer &lt;a href="https://reporails.com/rules/core/modality-weakness" rel="noopener noreferrer"&gt;hedged instructions&lt;/a&gt; ("should", "try to"), 4.7 per 100 against 5.8, and fewer or the same at every repo size. More of their headings &lt;a href="https://reporails.com/rules/core/heading-as-instruction" rel="noopener noreferrer"&gt;double as instructions&lt;/a&gt;, like &lt;code&gt;## Always Run Tests Before Pushing&lt;/code&gt;, at 5.4 per 100 against 3.4. And despite &lt;code&gt;NEVER&lt;/code&gt; and &lt;code&gt;DO NOT&lt;/code&gt;, they write slightly fewer prohibitions, 16.4% of their instructions against 18.4%. The most common of those capitals, &lt;code&gt;MUST&lt;/code&gt;, asks for something.&lt;/p&gt;

&lt;p&gt;Anthropic's and OpenAI's prompting guides both say newer models don't need the capitals. Anthropic's says its newer models are "more responsive to the system prompt" and may overtrigger on prompts written to push older ones: "The fix is to dial back any aggressive language. Where you might have said 'CRITICAL: You MUST use this tool when...', you can use more normal prompting like 'Use this tool when...'." OpenAI's GPT-5 guide has the same example from the other side. A line from Cursor's prompt, "Be THOROUGH when gathering information. Make sure you have the FULL picture before replying.", worked with older models and turned out "counterproductive with GPT-5, which is already naturally introspective and proactive at gathering context."&lt;/p&gt;

&lt;h2&gt;
  
  
  Emoji, "please", and thinking out loud
&lt;/h2&gt;

&lt;p&gt;1,541 repos (15.8%) use emoji in their instruction files, 29,311 times in total. Like the ALL-CAPS repos, more of their headings double as instructions.&lt;/p&gt;

&lt;p&gt;839 repos (8.6%) say "please" to their agent, 2,051 times. In small setups, politeness travels with softer instructions. In the smallest quarter of repos, "please" repos carry twice as many hedged instructions as the others, 8.7 per 100 against 4.3. In the largest setups the difference mostly disappears.&lt;/p&gt;

&lt;p&gt;798 repos (8.2%) write "step by step", "think hard" or &lt;code&gt;ultrathink&lt;/code&gt;. Almost all of it is "step by step", which covers both asking the agent to reason and describing a procedure in steps. Where it asks the agent to reason, reasoning models no longer need it. OpenAI's guide for them: "Since these models perform reasoning internally, prompting them to 'think step by step' or 'explain your reasoning' is unnecessary." &lt;code&gt;ultrathink&lt;/code&gt; and "think hard" turn up in a few dozen repos. In Claude Code, &lt;code&gt;ultrathink&lt;/code&gt; in a prompt requests deeper reasoning for that turn, while "think" and "think hard" pass "through as ordinary prompt text".&lt;/p&gt;

&lt;h2&gt;
  
  
  TODOs and hallucinations
&lt;/h2&gt;

&lt;p&gt;787 repos (8.0%) have &lt;code&gt;TODO&lt;/code&gt;, &lt;code&gt;TBD&lt;/code&gt; or &lt;code&gt;FIXME&lt;/code&gt; in their instruction files. Some of those are rules about TODO comments in code. Others are the file's own unfinished business, shipped to the agent as-is.&lt;/p&gt;

&lt;p&gt;246 repos (2.5%) mention hallucinating in their instruction files, some as a rule ("don't hallucinate") and some because the project itself works with model output. A bare "don't hallucinate" &lt;a href="https://reporails.com/rules/core/specificity-gap" rel="noopener noreferrer"&gt;names nothing&lt;/a&gt; to check an answer against. Naming the source does: the schema file, the API docs page, the command that lists what exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never force-push
&lt;/h2&gt;

&lt;p&gt;360 repos (3.7%) write a rule against a destructive command, such as force-pushing, &lt;code&gt;rm -rf&lt;/code&gt;, &lt;code&gt;git reset --hard&lt;/code&gt; or dropping a table. The line from one machine-learning project is typical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NEVER push to &lt;code&gt;main&lt;/code&gt;. NEVER force push.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a rule the agent can follow or miss, and a force-push is the kind of miss you'd rather not find out about after the fact. A hook, a script the agent's harness runs at a set moment such as before a tool call, can stop the command before it runs. Claude Code can also refuse it with a deny rule in the project's &lt;code&gt;.claude/settings.json&lt;/code&gt;. 72 of the 360 repos (20%) ship an agent hook of some kind, against 7.6% of all repos, and 6 deny a destructive command in a committed settings file. (&lt;a href="https://reporails.com/articles/deterministic-guardrails-prompts-steer-hooks-enforce" rel="noopener noreferrer"&gt;Prompts steer, hooks enforce&lt;/a&gt; covers the difference.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr881b8kjfp6i20a59sjq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr881b8kjfp6i20a59sjq.png" alt="Of the 360 repos with a rule against a destructive command in their instruction files, 72 (20%) ship an agent hook of any kind, and 6 (1.7%) deny the command in a committed .claude/settings.json. For comparison, 7.6% of all 9,784 repos ship an agent hook." width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repos that write these rules are firmer elsewhere too, with 3.9 hedged instructions per 100 against 5.2, fewer at every repo size. They also carry more of one finding at every size: a "never" written more strongly than &lt;a href="https://reporails.com/rules/core/direction-imbalance" rel="noopener noreferrer"&gt;the permission that goes with it&lt;/a&gt;, most clearly in small and mid-size setups. The agent may then apply the prohibition more widely than intended, unless the case where the thing is allowed is written as clearly.&lt;/p&gt;

&lt;p&gt;The settings files have habits of their own. Of 1,466 committed Claude Code settings files, 19 allow any shell command, and 3 turn permission prompts off entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 129,121-word &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The median repo's largest main instruction file is 697 words. 57 repos have one over 10,000.&lt;/p&gt;

&lt;p&gt;The largest runs to 129,121 words. It opens with the &lt;a href="https://reporails.com/articles/state-of-ai-instruction-quality-init-files-score-higher" rel="noopener noreferrer"&gt;&lt;code&gt;/init&lt;/code&gt; line&lt;/a&gt;, and its Architecture section alone is 107,019 words across 374 lines, close to 300 words a line. The repo is an autonomous coding-agent project, and its &lt;code&gt;CLAUDE.md&lt;/code&gt; has grown with it, one architecture note at a time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjjx1djlf8n1zzdjqlkk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjjx1djlf8n1zzdjqlkk.png" alt="Largest main instruction file per repo, in words: 697 for the median repo, 129,121 for the largest, a CLAUDE.md whose Architecture section alone is 107,019 words across 374 lines. 57 repos have a main instruction file over 10,000 words." width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt; "at the start of every session", and its docs set a target of "under 200 lines per CLAUDE.md file. Longer files consume more context and reduce adherence." At 129,121 words, the file is well over 100,000 tokens before the agent has read a line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yours
&lt;/h2&gt;

&lt;p&gt;When a rule isn't being followed, look first at the places this post found: a capitalized line that fires everywhere, a heading doing an instruction's job, a "please" in front of a "maybe", a TODO that never got finished, a file long enough that the rule you care about is a few thousand words in.&lt;/p&gt;

&lt;p&gt;The checker flags &lt;a href="https://reporails.com/rules/core/no-ephemeral-content" rel="noopener noreferrer"&gt;a &lt;code&gt;TODO&lt;/code&gt; left in the file&lt;/a&gt;, &lt;a href="https://reporails.com/rules/core/direction-imbalance" rel="noopener noreferrer"&gt;a "never" with no matching permission&lt;/a&gt; and &lt;a href="https://reporails.com/rules/core/modality-weakness" rel="noopener noreferrer"&gt;a hedged "try to"&lt;/a&gt; on the line where they sit, and it runs the checks behind this post's finding rates. Each run checks one agent's files; the second command switches to Copilot, and &lt;code&gt;cursor&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt; and &lt;code&gt;antigravity&lt;/code&gt; (for &lt;code&gt;GEMINI.md&lt;/code&gt;) work the same way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @reporails/cli check &lt;span class="nt"&gt;-v&lt;/span&gt;
npx @reporails/cli check &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--agent&lt;/span&gt; copilot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The capitals, the "please" and the file length are quick to count yourself. Swap in the files your agents read, such as &lt;code&gt;GEMINI.md&lt;/code&gt;, &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; or the files under &lt;code&gt;.cursor/rules/&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-cwE&lt;/span&gt; &lt;span class="s2"&gt;"IMPORTANT|CRITICAL|MUST|NEVER|ALWAYS|DO NOT"&lt;/span&gt; CLAUDE.md AGENTS.md
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-niE&lt;/span&gt; &lt;span class="s2"&gt;"please|step[- ]by[- ]step|ultrathink|TODO|FIXME|hallucinat"&lt;/span&gt; CLAUDE.md AGENTS.md
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; CLAUDE.md AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;From &lt;a href="https://reporails.com/articles/state-of-ai-instruction-quality-q3-2026" rel="noopener noreferrer"&gt;The State of AI Instruction Quality: Q3 2026&lt;/a&gt;: 10,000 of the most-starred GitHub repos, 15,237 agent setups, September 2026 snapshot.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Habits are counted in each repo's instruction files with code blocks removed; shares are of the 9,784 repos with a setup. ALL-CAPS counts the words IMPORTANT, CRITICAL, MUST, NEVER, ALWAYS, DO NOT, DON'T, YOU MUST, MANDATORY and REQUIRED in capitals. Finding rates count findings per 100 directives and constraints, in repos with at least 5; "every repo size" means each of four equal groups of repos by setup size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;None of the habits moves the overall score in a consistent direction: compared at similar repo size, the differences shift from one size group to the next.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt;, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the instruction files your coding agent loads, from &lt;code&gt;CLAUDE.md&lt;/code&gt; to Copilot and Cursor rules, measures which instructions couple to behavior, and points to the lines worth tightening.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>agents</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>State of AI Instruction Quality: /init Files Score Higher</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:07:14 +0000</pubDate>
      <link>https://dev.to/reporails/state-of-ai-instruction-quality-init-files-score-higher-1a1h</link>
      <guid>https://dev.to/reporails/state-of-ai-instruction-quality-init-files-score-higher-1a1h</guid>
      <description>&lt;p&gt;The &lt;code&gt;CLAUDE.md&lt;/code&gt; files that started from Claude Code's &lt;code&gt;/init&lt;/code&gt; score higher than the rest, at every file size.&lt;/p&gt;

&lt;p&gt;You can tell them by the line &lt;code&gt;/init&lt;/code&gt; puts at the top:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We scored 8,220 &lt;code&gt;CLAUDE.md&lt;/code&gt; files from the 10,000 most-starred repos on GitHub for &lt;a href="https://reporails.com/articles/state-of-ai-instruction-quality-q3-2026" rel="noopener noreferrer"&gt;the Q3 report&lt;/a&gt;. One in five has that line. On &lt;code&gt;ails check&lt;/code&gt;'s 0 to 10 scale, their median is 8.4, against 7.7 for the rest. Six in ten of them score 8 or above. Of the rest, fewer than half do.&lt;/p&gt;

&lt;p&gt;A short file has fewer chances to pick up findings, so we split all 8,220 into four equal groups by size. The &lt;code&gt;/init&lt;/code&gt; files lead in every group, by 0.6 to 0.9 points.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftywnokm13aqw79pengtt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftywnokm13aqw79pengtt.png" alt="Median CLAUDE.md score with and without the /init line, by file size: 8.6 vs 7.8 up to 37 items, 8.6 vs 7.7 at 38 to 75, 8.4 vs 7.7 at 76 to 137, 8.1 vs 7.5 over 137." width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A map, then a few rules
&lt;/h2&gt;

&lt;p&gt;Claude Code's docs describe what &lt;code&gt;/init&lt;/code&gt; produces: "Claude analyzes your codebase and creates a file with build commands, test instructions, and project conventions it discovers."&lt;/p&gt;

&lt;p&gt;That's what the files look like. About a fifth of what an &lt;code&gt;/init&lt;/code&gt; file says tells the agent what to do. The rest describes the repo: where things live, what each command runs, how the parts fit together. In other &lt;code&gt;CLAUDE.md&lt;/code&gt; files, the share of instructions is close to three in ten.&lt;/p&gt;

&lt;p&gt;The layout repeats from repo to repo. 87% of &lt;code&gt;/init&lt;/code&gt; files have an Architecture section and 84% a Commands section. Among the other files, about one in five has each. Nearly nine in ten &lt;code&gt;/init&lt;/code&gt; files carry at least one code block, against 44% of the others, and that gap holds at every file size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where its rules go wrong
&lt;/h2&gt;

&lt;p&gt;Fewer rules leave fewer places for a rule to go wrong, and the findings show it. At every file size, fewer &lt;code&gt;/init&lt;/code&gt; files carry a hedged instruction ("should", "try to"), and fewer carry a "never" written more strongly than the permission that goes with it, like "NEVER push to remote" next to "you can push if asked". Prohibitions are also a smaller share of their instructions: 16.8%, against 21.2%.&lt;/p&gt;

&lt;p&gt;About one instruction in four still &lt;a href="https://reporails.com/rules/core/specificity-gap" rel="noopener noreferrer"&gt;names no file, command or tool&lt;/a&gt;, against nearly one in three in other &lt;code&gt;CLAUDE.md&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;One finding runs the other way. A file that's mostly description puts prose on the same topic right next to its instructions, and the checker flags an instruction &lt;a href="https://reporails.com/rules/core/content-dilution" rel="noopener noreferrer"&gt;buried in it&lt;/a&gt;: 17.7 times per 100 instructions in &lt;code&gt;/init&lt;/code&gt; files, against 10.5 elsewhere. At no file size do &lt;code&gt;/init&lt;/code&gt; files come out ahead on this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with yours
&lt;/h2&gt;

&lt;p&gt;You can delete the line. The checker reads it as description. It marks how the file started, and removing it costs nothing we can measure.&lt;/p&gt;

&lt;p&gt;Keep the shape. It does some of the work: 646 &lt;code&gt;CLAUDE.md&lt;/code&gt; files without the line have both an Architecture and a Commands section, and their median is 7.9. That's above the 7.7 of all files without the line, and under the 8.4 of files with it.&lt;/p&gt;

&lt;p&gt;The same shape works in an &lt;code&gt;AGENTS.md&lt;/code&gt; or a &lt;code&gt;GEMINI.md&lt;/code&gt;: a map of the repo, its commands in code blocks and a short list of rules. We measured it on &lt;code&gt;CLAUDE.md&lt;/code&gt; because that's the file &lt;code&gt;/init&lt;/code&gt; leaves a marker in.&lt;/p&gt;

&lt;p&gt;Six in ten &lt;code&gt;/init&lt;/code&gt; files are the only instruction file in their setup, against three in ten other &lt;code&gt;CLAUDE.md&lt;/code&gt; files, so for many repos the map, the commands and a handful of rules are everything the agent gets. Right after describing &lt;code&gt;/init&lt;/code&gt;, the docs add: "Refine from there with instructions Claude wouldn't discover on its own." That's the part you write by hand. When you add a rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give it what &lt;code&gt;/init&lt;/code&gt; would have: the command, in a code block.&lt;/li&gt;
&lt;li&gt;If it's a "never", write the case where the agent may as clearly as the one where it may not.&lt;/li&gt;
&lt;li&gt;Keep the explanation around it short. The file already has plenty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see where your own file stands:&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;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt; CLAUDE.md
npx @reporails/cli check &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command shows whether your file started from &lt;code&gt;/init&lt;/code&gt;. The second scores it and lists each finding with its line number. Run &lt;code&gt;git blame CLAUDE.md&lt;/code&gt; on those lines to see whether they came with the first version of the file or were added later.&lt;/p&gt;

&lt;p&gt;One more thing about the line. The longest main instruction file in all 10,000 repos, 129,121 words, opens with it too. That one is in the habits post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;From &lt;a href="https://reporails.com/articles/state-of-ai-instruction-quality-q3-2026" rel="noopener noreferrer"&gt;The State of AI Instruction Quality: Q3 2026&lt;/a&gt;: 10,000 of the most-starred GitHub repos, 15,237 agent setups, September 2026 snapshot. Scores are from that run; the checker has been updated since, so your own file may score a little differently.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;/init&lt;/code&gt; files are &lt;code&gt;CLAUDE.md&lt;/code&gt; files with the &lt;code&gt;/init&lt;/code&gt; line in their first 4,000 characters: 1,641 files in 1,539 repos. Counting every instruction file, 1,604 repos carry the line. File-size groups cut at 37, 75 and 137 items. Finding rates count findings per 100 directives and constraints, the same denominator as the report.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Counted per repo instead of per file, the gap narrows as setups grow, from 1.3 points in the smallest quarter of setups to 0.2 in the largest. The Architecture-and-Commands shape accounts for part of the gap; the files alone don't show what accounts for the rest.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt;, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. If your &lt;code&gt;CLAUDE.md&lt;/code&gt; started from &lt;code&gt;/init&lt;/code&gt;, it starts from the shape that scored higher here. Reporails reads the rest of what you wrote, measures which instructions couple to behavior, and points to the lines worth tightening.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>analytics</category>
      <category>agents</category>
    </item>
    <item>
      <title>The State of AI Instruction Quality: Q3 2026</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Tue, 13 Oct 2026 16:21:27 +0000</pubDate>
      <link>https://dev.to/reporails/the-state-of-ai-instruction-quality-q3-2026-19bg</link>
      <guid>https://dev.to/reporails/the-state-of-ai-instruction-quality-q3-2026-19bg</guid>
      <description>&lt;p&gt;91% of the most-starred repos on GitHub give their coding agent at least one instruction that never says which file, command or tool it means.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reporails.com/articles/state-ai-instruction-quality-30k-repo-analysis" rel="noopener noreferrer"&gt;In April we measured 28,721 repos&lt;/a&gt;, and our own limitations section admitted that a 10-star side project counted the same as a 50,000-star production repo. This time we started from the other end: the 10,000 most-starred English-language repos that ship instructions for a coding agent. If you've ever opened a well-known repo to see how they wrote their &lt;code&gt;AGENTS.md&lt;/code&gt;, these are the files you were looking at.&lt;/p&gt;

&lt;p&gt;10,000 repos. 15,237 agent setups. Read in September 2026 and scored with the same &lt;code&gt;ails check&lt;/code&gt; you can run on your own repo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;91.1% of repos have at least one vague instruction. Among the lines that tell the agent what to do, about 1 in 3 is vague.&lt;/li&gt;
&lt;li&gt;Vague is the most common finding of several: 96.6% of repos have at least one finding, 38 in the median repo.&lt;/li&gt;
&lt;li&gt;Star count doesn't predict the score (Spearman's rho -0.028 across 9,674 repos).&lt;/li&gt;
&lt;li&gt;Sub-agent files score lowest: 19.6% of them score below 5, against 6.8% of main files.&lt;/li&gt;
&lt;li&gt;Only 7.6% of repos back their instructions with an agent hook.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  91% of repos carry at least one vague instruction
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;AGENTS.md&lt;/code&gt; from a popular Python project runs to 386 lines, and most of it is careful work. It names &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;torch.inference_mode&lt;/code&gt; and dozens of the project's own modules the agent can look up. It also has two rules about dependencies:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3cs6v9csb1cjdsatoch.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3cs6v9csb1cjdsatoch.png" alt="An AGENTS.md from a popular Python project, lines 11 to 12: " width="799" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both are sensible. The second one gives the agent a file to open and a condition to test. The first leaves "absolutely necessary" for the agent to decide on its own, mid-task, with nothing in the file to check its call against.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ails check&lt;/code&gt; flags the first one as &lt;a href="https://reporails.com/rules/core/specificity-gap" rel="noopener noreferrer"&gt;&lt;code&gt;CORE:C:0042&lt;/code&gt;&lt;/a&gt;, a vague instruction: a directive or constraint that names no file, command or tool, in a section where other lines do. The second one passes.&lt;/p&gt;

&lt;p&gt;Another popular repo's Copilot instructions ask for the same kind of judgment call:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When there is an option, opt for more performant solutions and trade RAM usage for less CPU cycles&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which option? Measured how? The agent decides, and you find out what it decided when you read the diff.&lt;/p&gt;

&lt;p&gt;Across the 9,776 repos with a setup we could check, 8,910 carry at least one &lt;code&gt;CORE:C:0042&lt;/code&gt; (91.1%).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyriku485b3huc5rznvb8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyriku485b3huc5rznvb8.png" alt="A grid of 100 squares with 91 filled: 91.1% of 9,776 repos carry at least one vague instruction." width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The checker raised that finding 655,791 times. Two-thirds of what sits in these files describes, explains or shows; a third tells the agent what to do. Count only those directives and constraints and about one in three is vague.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw0sfwszuze6vutbllyw2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw0sfwszuze6vutbllyw2.png" alt="Every 100 items in an instruction file: 66 describe, explain or show; 34 tell the agent what to do, and 11 of those are vague." width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vague instructions are the most common finding, and far from the only one. 96.6% of repos have at least one finding of some kind, 38 in the median repo, and four other checks fire in more than 60% of repos.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0yvhgv8hpyggc4mwpq0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0yvhgv8hpyggc4mwpq0.png" alt="Share of 9,784 repos with each finding: Specificity Gap, CORE:C:0042, names no file, command or tool, 91.1%; Instruction Elaboration, CORE:E:0004, too short to register, 87.8%; Modality Weakness, CORE:C:0043, hedged, 70.3%; Content Dilution, CORE:C:0041, 62.5%; Direction Imbalance, CORE:D:0002, 61.8%; Topic Overlap, CORE:C:0044, 33.1%." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stars don't predict the score
&lt;/h2&gt;

&lt;p&gt;Every file and every setup &lt;code&gt;ails check&lt;/code&gt; reads gets a score from 0 to 10, built from all of its findings (&lt;a href="https://reporails.com/docs/score-guide" rel="noopener noreferrer"&gt;how the score works&lt;/a&gt;). If popularity bought better instructions, scores would climb with stars. From 493 stars to 454,882, the line stays flat.&lt;/p&gt;

&lt;p&gt;Spearman's rho between stars and score is -0.028 across 9,674 repos: stars account for less than 0.1% of the variation. Split the repos into ten equal groups by stars and every group lands in the same place, with 63% to 66% of repos scoring 7 or above. The most-starred tenth, 12,849 stars and up, averages a touch lower than the least-starred tenth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyykmyubig47fm44r8bk7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyykmyubig47fm44r8bk7.png" alt="Share of repos scoring 7 or above, by star decile: flat between 63% and 66% from 493 stars to 454,882." width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A famous repo's &lt;code&gt;AGENTS.md&lt;/code&gt; scores like anyone else's. If you copy one, read it line by line first.&lt;/p&gt;

&lt;p&gt;Next: does the agent you write for change the picture?&lt;/p&gt;

&lt;h2&gt;
  
  
  Who writes for which agent
&lt;/h2&gt;

&lt;p&gt;Two setups dominate: &lt;code&gt;AGENTS.md&lt;/code&gt; in 62.6% of repos and Claude Code in 61.6%. Copilot follows at 17.9%, then Cursor, Gemini and Codex. 4,213 repos (43.1%) write for more than one agent, most often the pair of Claude Code and &lt;code&gt;AGENTS.md&lt;/code&gt; (2,399 repos).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe61o6b98enqh5jiirzqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe61o6b98enqh5jiirzqp.png" alt="Left: share of 9,784 repos with a setup for each agent, AGENTS.md 62.6%, Claude Code 61.6%, GitHub Copilot 17.9%, Cursor 7.5%, Gemini 4.4%, Codex 1.7%. Right: agents per repo, 1 agent 5,571 repos, 2 agents 3,201, 3 agents 822, 4 agents 152, 5 agents 38." width="799" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The formats differ in what they're made of. Skills are the largest share of files for every agent except Gemini, and Cursor is the only one where rule files outnumber main files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffc50t4cmcsafge3l82b5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffc50t4cmcsafge3l82b5.png" alt="Share of instruction files by type for each agent. Skills are the largest share for Claude Code (39%), AGENTS.md (51%), Copilot (46%), Cursor (53%) and Codex (63%); Gemini's main files lead at 47%. Cursor's rules (23%) outnumber its main files (16%)." width="799" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The quality of what's in them barely moves between agents. The vague rate sits between 32 and 36 of every 100 instructions, whichever agent the file was written for.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Setups&lt;/th&gt;
&lt;th&gt;With a vague instruction&lt;/th&gt;
&lt;th&gt;Vague per 100&lt;/th&gt;
&lt;th&gt;Below 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;6,030&lt;/td&gt;
&lt;td&gt;87.4%&lt;/td&gt;
&lt;td&gt;33.7&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt; (shared)&lt;/td&gt;
&lt;td&gt;6,125&lt;/td&gt;
&lt;td&gt;91.0%&lt;/td&gt;
&lt;td&gt;32.5&lt;/td&gt;
&lt;td&gt;5.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;1,756&lt;/td&gt;
&lt;td&gt;94.4%&lt;/td&gt;
&lt;td&gt;34.6&lt;/td&gt;
&lt;td&gt;4.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;734&lt;/td&gt;
&lt;td&gt;93.2%&lt;/td&gt;
&lt;td&gt;33.4&lt;/td&gt;
&lt;td&gt;9.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini (Antigravity)&lt;/td&gt;
&lt;td&gt;430&lt;/td&gt;
&lt;td&gt;91.4%&lt;/td&gt;
&lt;td&gt;34.2&lt;/td&gt;
&lt;td&gt;8.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;162&lt;/td&gt;
&lt;td&gt;96.3%&lt;/td&gt;
&lt;td&gt;36.0&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Codex counts repos with a &lt;code&gt;.codex/&lt;/code&gt; folder; a plain &lt;code&gt;AGENTS.md&lt;/code&gt; setup sits in the shared row.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftonqh3fo42qy6lm0vj2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftonqh3fo42qy6lm0vj2f.png" alt="Vague instructions per 100, by agent: every agent between 32 and 36." width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The one column that does move is the last: Cursor and Gemini setups land below 5 about twice as often as the rest.&lt;/p&gt;

&lt;p&gt;Where the numbers do move is between the files inside a setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sub-agent files score lowest
&lt;/h2&gt;

&lt;p&gt;A sub-agent file defines a specialist the main agent hands work to: a reviewer, a debugger, a test writer. Those files are where the low scores pile up. 19.6% of sub-agent files score below 5, against 6.8% of main files like &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;AGENTS.md&lt;/code&gt;, nearly three times the rate. Commands and prompts (13.9%), rules (13.6%) and skills (12.1%) sit in between.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qw35arg3ash59qvyayt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0qw35arg3ash59qvyayt.png" alt="Share of files scoring below 5, by file type: sub-agents 19.6%, commands and prompts 13.9%, rules 13.6%, skills 12.1%, main files 6.8%." width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The three biggest sub-agent folders all sit above the main-file rate, and Claude Code's is the worst: nearly one in four of its sub-agent files scores below 5.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5autaaj3ot5wx1e3ixps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5autaaj3ot5wx1e3ixps.png" alt="Share of sub-agent files scoring below 5, by folder: .claude/agents 22.9% of 1,914 files, .codex/agents 19.9% of 181, .github/agents 10.9% of 671, against 6.8% of 25,095 main files." width="799" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sub-agent files are also the busiest. The median one carries 19 directives, against 12 in a main file.&lt;/p&gt;

&lt;p&gt;One Copilot bug-fixing sub-agent, in a large TypeScript monorepo, scores 3.9. Its quality gates:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl7xlumm0jogq86mmht5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl7xlumm0jogq86mmht5.png" alt="A Copilot bug-fixing sub-agent in .github/agents/, lines 37 to 41: " width="799" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four gates, and nowhere in the file's 212 lines is the command for any of them. The agent has to work out how this monorepo tests, lints, type-checks and builds before it can obey a single one.&lt;/p&gt;

&lt;p&gt;A Claude Code debugging sub-agent in another well-known repo scores 3.6. It opens:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are an expert software debugger who treats debugging as a scientific process, not guesswork.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In 41 lines, the only file it names is &lt;code&gt;CODE_DEBUGGING_SESSION.md&lt;/code&gt;, the report it's told to write. Nothing in it is about the project it lives in; it would read the same in any repo. It also carries the line most sub-agents do: 57.4% of sub-agent files have a line that starts with "You are a", "You are an" or "You are the".&lt;/p&gt;

&lt;p&gt;To check yours, run &lt;code&gt;npx @reporails/cli check&lt;/code&gt; and read the per-file scores for your &lt;code&gt;.claude/agents/&lt;/code&gt;, &lt;code&gt;.github/agents/&lt;/code&gt; or &lt;code&gt;.codex/agents/&lt;/code&gt; folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  7.6% of repos back their instructions with a hook
&lt;/h2&gt;

&lt;p&gt;Everything above is about instructions, which the agent can follow or not. A hook is different: a script the agent's harness runs at a set moment, such as before a tool call, that can block what an instruction only asks for. (&lt;a href="https://reporails.com/articles/deterministic-guardrails-prompts-steer-hooks-enforce" rel="noopener noreferrer"&gt;Prompts steer, hooks enforce&lt;/a&gt; is the long version.)&lt;/p&gt;

&lt;p&gt;746 of the 9,784 repos (7.6%) ship an agent hook of any kind. Claude Code is where they live: 591 of its 6,030 setups (9.8%) have one. Cursor follows at 4.9%, Copilot at 1.3% and Gemini at 0.9%.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdeoaebrzs4xdpslm82al.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdeoaebrzs4xdpslm82al.png" alt="Share of each agent's setups with a hook: Claude Code 9.8% (591 of 6,030), Cursor 4.9% (36 of 734), GitHub Copilot 1.3% (23 of 1,756), Gemini 0.9% (4 of 430); any agent across all repos 7.6% (746 of 9,784). Codex left out because its setups are counted only in repos with a .codex/ folder." width="799" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the other 92%, nothing at the agent's own level checks whether the instructions were followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What held from April
&lt;/h2&gt;

&lt;p&gt;April's 28,721 repos came from GitHub search. This quarter's 10,000 are the most-starred, read with a newer version of the checker, so the comparison is about direction, not size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;April (28,721 repos)&lt;/th&gt;
&lt;th&gt;Q3 2026 (10,000 most-starred)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;37% of projects configure more than one agent&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Held.&lt;/strong&gt; 43.1% of repos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The median instruction file: 50 items, 12 of them directives&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Held.&lt;/strong&gt; The median main file: 55 items, 12 directives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt; is the most common instruction file&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Held.&lt;/strong&gt; &lt;code&gt;AGENTS.md&lt;/code&gt; (11,248) still leads &lt;code&gt;CLAUDE.md&lt;/code&gt; (8,838); &lt;code&gt;SKILL.md&lt;/code&gt; (13,361) now outnumbers both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor setups are rules-heavy&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Held.&lt;/strong&gt; Cursor rule files (1,820) outnumber Cursor main files (1,260)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-agents carry the most directives&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Held.&lt;/strong&gt; Median 19 per sub-agent file, against 12 in main files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-agents are almost entirely persona prompts&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Mostly.&lt;/strong&gt; 57.4% have a line starting "You are a/an/the"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;code-reviewer.md&lt;/code&gt; is the most common sub-agent&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Held.&lt;/strong&gt; Still the most common, in 41 repos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;89.9% of configurations carry a vague instruction&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Held.&lt;/strong&gt; 91.1% of repos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The most-copied skills are the vaguest&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Didn't hold.&lt;/strong&gt; No link between how widely a skill is copied and how vague it is (188 skills copied into 5 or more repos)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How we measured
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The sample.&lt;/strong&gt; 10,000 English-language GitHub repos, taken top-down by stars from repos that ship an instruction file for &lt;a href="https://reporails.com/docs/agent-support" rel="noopener noreferrer"&gt;Claude Code, Codex, Copilot, Cursor or Gemini&lt;/a&gt;. Stars run from 493 to 454,882, with a median of 1,551. Files are the September 2026 snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What we read.&lt;/strong&gt; We read every repository the way its coding agent does: the instruction files each agent loads by default, at the exact names and paths its documentation specifies. That is &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, rules, skills, sub-agents and commands, across 15,237 agent setups in 9,784 repos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imports.&lt;/strong&gt; The text of a Claude Code &lt;code&gt;@path&lt;/code&gt; import whose target is in the snapshot is read as part of the importing file. Imports written with a leading dot directory (&lt;code&gt;@.github/…&lt;/code&gt;) are not recognized and stay literal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out of scope.&lt;/strong&gt; Repo code, and docs an agent opens only when something links to them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scorer.&lt;/strong&gt; Every score comes from the same &lt;code&gt;ails check&lt;/code&gt; pipeline you can &lt;a href="https://reporails.com/docs/getting-started" rel="noopener noreferrer"&gt;run on your own repo&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Denominators.&lt;/strong&gt; Shares of repos, like the 91%, count the 9,776 repos with a checked setup. The vague rate, about 1 in 3, counts against the directives and constraints in those setups: 655,791 findings over 1,894,268 instructions. Score figures count the 9,674 repos with a score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not in the figures.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;158 of the largest agent setups (more than 90 instruction files each, about 1% of setups) are not in these figures. 77 are larger than the 10,000 instruction units &lt;code&gt;ails check&lt;/code&gt; currently scores, and 2 exceed its time limit; they are skill and rule libraries, with a median of 15,027 units across 192 files. The other 79 were scored after these figures were frozen and will be added in a dataset update.&lt;/li&gt;
&lt;li&gt;258 setups contain no directive or constraint (every line is descriptive, or the file is empty), so the scorer returns no score for them.&lt;/li&gt;
&lt;li&gt;18 setups couldn't be checked: 8 are larger than the same 10,000-unit limit and 10 hit an error during scoring.&lt;/li&gt;
&lt;li&gt;For 76 repos we couldn't pin the files in sub-folders (sub-agents, commands, hooks) to the September snapshot, so they're measured on the files we already had.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it measures.&lt;/strong&gt; How the instructions are written, not what any agent did with them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check your own files
&lt;/h2&gt;

&lt;p&gt;If you came here to find out whose &lt;code&gt;AGENTS.md&lt;/code&gt; to copy: the star count won't tell you, so read the lines instead, or let the checker read them. To &lt;a href="https://reporails.com/docs/getting-started" rel="noopener noreferrer"&gt;run the same check&lt;/a&gt; we ran on all 10,000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @reporails/cli check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lists the findings file by file, with line numbers, and a &lt;a href="https://reporails.com/docs/score-guide" rel="noopener noreferrer"&gt;0 to 10 score&lt;/a&gt; for each file. Start with the three findings most repos carry: instructions that name no file, command or tool (&lt;a href="https://reporails.com/rules/core/specificity-gap" rel="noopener noreferrer"&gt;&lt;code&gt;CORE:C:0042&lt;/code&gt;&lt;/a&gt;), instructions too short to register (&lt;a href="https://reporails.com/rules/core/instruction-elaboration" rel="noopener noreferrer"&gt;&lt;code&gt;CORE:E:0004&lt;/code&gt;&lt;/a&gt;), and hedged ones, the "should" and "try to" kind (&lt;a href="https://reporails.com/rules/core/modality-weakness" rel="noopener noreferrer"&gt;&lt;code&gt;CORE:C:0043&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The data behind these figures is published in the &lt;a href="https://github.com/reporails/reports/tree/main/2026-Q3" rel="noopener noreferrer"&gt;&lt;code&gt;2026-Q3&lt;/code&gt; folder of reporails/reports&lt;/a&gt;. If you disagree with a finding, count the rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next in this series
&lt;/h2&gt;

&lt;p&gt;Four shorter posts follow over the next days, each on one habit from the same 10,000 repos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;/init&lt;/code&gt; header.&lt;/strong&gt; 1,604 repos (16.4%) still open with the line Claude Code's &lt;code&gt;/init&lt;/code&gt; writes, and they score higher than the repos without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"You are…"&lt;/strong&gt; 285 main files tell the agent who it is with a "You are a…" line. They score lower at every file size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The habits.&lt;/strong&gt; ALL-CAPS in a third of repos, emoji, "please", TODOs, and a 129,121-word main file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden attribution.&lt;/strong&gt; About 400 repos tell the agent not to take credit for its work.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>analytics</category>
      <category>agents</category>
    </item>
    <item>
      <title>Progressive Disclosure: Why Isn't Claude Code Following My Instructions?</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:17:36 +0000</pubDate>
      <link>https://dev.to/reporails/progressive-disclosure-why-isnt-claude-code-following-my-instructions-4845</link>
      <guid>https://dev.to/reporails/progressive-disclosure-why-isnt-claude-code-following-my-instructions-4845</guid>
      <description>&lt;p&gt;The last piece (&lt;a href="https://reporails.com/articles/progressive-disclosure-claude-code-hooks-events-telemetry" rel="noopener noreferrer"&gt;Progressive Disclosure: Claude Code Hooks, Events and Telemetry&lt;/a&gt;) sent every hook event through one bus and wrote it all down. It ended on what that log can't tell you: whether a rule that loaded "was worded well enough to steer anything".&lt;/p&gt;

&lt;p&gt;So I loaded one spec, worded two ways, into the same 16-turn Claude Code session, three times each, and read what came out at the end. Here's the last migration doc from each, written with roughly 120K to 140K tokens of work piled on top of the spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Release: 4.2.0

&lt;span class="gh"&gt;# 0005: Index cards.created_at&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 0005: Index cards by creation time&lt;/span&gt;

Adds &lt;span class="sb"&gt;`cards_created_at_idx`&lt;/span&gt; on &lt;span class="sb"&gt;`cards (created_at)`&lt;/span&gt; so the nightly report can find one day's cards without scanning the whole table.

&lt;span class="gs"&gt;**Release:**&lt;/span&gt; TBD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both specs asked for the release. The bus log shows each one delivered once, on the first prompt, and nothing took it out of the context after that. The named spec put &lt;code&gt;Release: 4.2.0&lt;/code&gt; on every migration doc. The vague one put &lt;code&gt;TBD&lt;/code&gt; there every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same spec, two wordings
&lt;/h2&gt;

&lt;p&gt;The demo is a small Node payments service with two legacy knex migrations. Neither has a &lt;code&gt;down&lt;/code&gt; step, and neither doc says which release it went out in, so whatever the agent does about either comes from the spec.&lt;/p&gt;

&lt;p&gt;The spec lives in &lt;code&gt;specs/migrations.md&lt;/code&gt;, and the bus from last time delivers it. One step is new: a prompt that starts with &lt;code&gt;#migration&lt;/code&gt; gets the spec printed into its context, and the log records the delivery. Whatever a &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook prints goes in front of the model before it starts (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Every hook event comes through here. Step one: write down what happened.&lt;/span&gt;
&lt;span class="k"&gt;in&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="nv"&gt;log&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&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;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/log.jsonl"&lt;/span&gt;
jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'{ts: (now | todate), event: .hook_event_name, tool: .tool_name,
        file: (.tool_input.file_path // .file_path), reason: .load_reason}
       | with_entries(select(.value != null))'&lt;/span&gt; &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;$in&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;$log&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# Step two: a prompt tagged #migration gets the migration spec.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.hook_event_name == "UserPromptSubmit" and (.prompt | startswith("#migration"))'&lt;/span&gt; &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;$in&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &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;$CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/specs/migrations.md"&lt;/span&gt;
  jq &lt;span class="nt"&gt;-nc&lt;/span&gt; &lt;span class="s1"&gt;'{ts: (now | todate), event: "delivered", file: "specs/migrations.md"}'&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;$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;The two versions ask for the same two things: a way back from every migration, and the release named in its doc.&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="gh"&gt;# Migration spec&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Every file in &lt;span class="sb"&gt;`db/migrations/`&lt;/span&gt; exports &lt;span class="sb"&gt;`up`&lt;/span&gt; and &lt;span class="sb"&gt;`down`&lt;/span&gt;. &lt;span class="sb"&gt;`down`&lt;/span&gt; undoes exactly what &lt;span class="sb"&gt;`up`&lt;/span&gt; did.
&lt;span class="p"&gt;-&lt;/span&gt; Every migration gets a doc in &lt;span class="sb"&gt;`docs/migrations/`&lt;/span&gt; with the same number. The doc's first line is &lt;span class="sb"&gt;`Release:`&lt;/span&gt; followed by the &lt;span class="sb"&gt;`version`&lt;/span&gt; from &lt;span class="sb"&gt;`package.json`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Migration notes&lt;/span&gt;

Migrations should be reversible where possible, so it's worth thinking about a way to roll each one back. It would also be good to keep the docs for migrations reasonably complete, ideally mentioning which release a change goes out in.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78d8wl578mrawmzq7z27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78d8wl578mrawmzq7z27.png" alt="The same migration spec in two wordings. The named version points at the db/migrations/ folder and says the doc's first line is Release: followed by the version from package.json. The vague version says migrations should be reversible where possible and that docs should ideally mention which release a change goes out in. Under each, the line Claude Code wrote on turn 16: Release: 4.2.0 for the named version, Release: TBD for the vague one." width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each session is 16 prompts, run headless with &lt;code&gt;claude -p&lt;/code&gt; on Claude Code 2.1.285 and &lt;code&gt;claude-opus-5-5&lt;/code&gt; at its default &lt;code&gt;medium&lt;/code&gt; effort. The first prompt starts the migration work and asks for one migration, turn 8 asks for a second and turn 16 for a third. In between, the agent does the rest of a working day: it groups the failures in an import log, finds and fixes bugs in a legacy importer, renames a column across the repository, the API serializer and five billing modules, writes tests, and runs the suite after each change. I ran three sessions per version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came back
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Turn 1&lt;/th&gt;
&lt;th&gt;Turn 8&lt;/th&gt;
&lt;th&gt;Turn 16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Named spec&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Release: 4.2.0&lt;/code&gt; in 3 of 3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Release: 4.2.0&lt;/code&gt; in 3 of 3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Release: 4.2.0&lt;/code&gt; in 3 of 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vague spec&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TBD&lt;/code&gt; in 3 of 3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TBD&lt;/code&gt; in 3 of 3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TBD&lt;/code&gt; in 3 of 3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The vague runs wrote &lt;code&gt;**Release:** TBD&lt;/code&gt;, or &lt;code&gt;Release: TBD (next release after 4.2.0)&lt;/code&gt;. That's a fair reading of "mention which release a change goes out in": the change hasn't shipped yet, so its release is the next one, whatever that turns out to be. What I wanted on that line was the version in &lt;code&gt;package.json&lt;/code&gt;, and only the named spec said so.&lt;/p&gt;

&lt;p&gt;Every migration in both sets got a &lt;code&gt;down&lt;/code&gt; step. Claude writes one for a knex migration whether you ask or not, so that half of the spec costs nothing to follow in either wording. The release line asks for something Claude wouldn't do on its own, and that's where the wording decided the result.&lt;/p&gt;

&lt;p&gt;Then I ran the same six sessions at &lt;code&gt;--effort low&lt;/code&gt;. The named spec still put &lt;code&gt;Release: 4.2.0&lt;/code&gt; on the first line of all nine migration docs. The vague one dropped the release altogether: none of its nine docs has a release line, not even &lt;code&gt;TBD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three sessions per version is a demonstration, and four shorter 8-turn sessions per version came out the same way. The measured effect comes from earlier controlled experiments, where specificity produced a 10.9x odds ratio in compliance (&lt;a href="https://reporails.com/articles/state-ai-instruction-quality-30k-repo-analysis" rel="noopener noreferrer"&gt;The State of AI Instruction Quality&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  It did what the spec said
&lt;/h2&gt;

&lt;p&gt;Claude Code's docs put it plainly: Claude treats &lt;code&gt;CLAUDE.md&lt;/code&gt; "as context, not enforced configuration, so how you write instructions affects how reliably Claude follows them", and there's "no guarantee of strict compliance, especially for vague or conflicting instructions" (&lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;How Claude remembers your project&lt;/a&gt;). A spec your bus delivers is context too.&lt;/p&gt;

&lt;p&gt;The vague spec asks for the release "ideally" and never says where the release comes from. So the model read it as the release still to come. One of the shorter runs wrote &lt;code&gt;**Release:** TBD (current package version is 4.2.0)&lt;/code&gt;: it had the version in front of it and still wrote &lt;code&gt;TBD&lt;/code&gt;. From the chair that looks exactly like Claude ignoring the spec. It was doing what the spec said, and the part I meant was never in the words.&lt;/p&gt;

&lt;p&gt;It's also the common case. In our analysis of 28,721 repositories, 89.9% of agent configurations contain at least one instruction that doesn't name what it means (same article).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the fade comes in
&lt;/h2&gt;

&lt;p&gt;I went in expecting the other failure as well: a spec that holds early and fades as the turns pile up. By turn 16 the context held the import log, the importer, the billing modules and a dozen test runs, and neither spec had moved. Whatever a version did on turn 1, it still did on turn 16.&lt;/p&gt;

&lt;p&gt;Long enough sessions do get there. In the decay piece (&lt;a href="https://dev.to/cleverhoods/see-how-ai-instructions-decay-then-write-ones-that-hold-k9"&gt;See how AI instructions decay, then write ones that hold&lt;/a&gt;), &lt;code&gt;Never write directly to the database&lt;/code&gt; was still sitting in the prompt when the agent, a long session later, wrote directly to the database. The model leans on what it read most recently. It's the same recency tendency that, inside one file, swings how often a rule is obeyed by about 90 points when you move it from the top to the bottom (&lt;a href="https://reporails.com/articles/opus-5-cost-instruction-conflicts" rel="noopener noreferrer"&gt;Opus 5: Cost of Instruction Conflicts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Here's the picture I use for it. Each hill is an instruction and its height is how well it's written. The water is everything the session piles on. Drag the load up and watch which hills go under first.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/editor/G-bor-M-sz-ros-the-reactor/embed/019f4f15-7353-7caa-a6d8-7f4eaa717276?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In my runs the vague spec never stood above the water, not even on turn 1. The named one still wrote the exact line on turn 16 of every run. Both outcomes were decided by what I wrote in the spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking what you load
&lt;/h2&gt;

&lt;p&gt;The bus decides when a spec loads and never reads what's in it. You can check one spec by eye. It stops working at the size real setups reach: in the same 30k analysis, sub-agent definitions have a median of 17 directives per file, and 17% of their instructions name a specific construct. Add the rules, the skills, the specs your bus delivers, re-read all of it after every change, and the re-reading is the part that doesn't happen.&lt;/p&gt;

&lt;p&gt;Claude Code ships one check for this. &lt;code&gt;/doctor prompt-audit&lt;/code&gt; (Claude Code 2.1.283 and later) has Claude read your &lt;code&gt;CLAUDE.md&lt;/code&gt;, rules, skills, commands, subagents and output styles and propose edits (&lt;a href="https://code.claude.com/docs/en/memory#audit-your-instruction-files" rel="noopener noreferrer"&gt;Audit your instruction files&lt;/a&gt;). On an earlier version of this demo it flagged the same kind of vague lines, and a dead path glob like the one further down. It runs inside a Claude session, on your tokens.&lt;/p&gt;

&lt;p&gt;Reporails, the tool I work on, reads instruction files with a fixed rule set. &lt;code&gt;ails check&lt;/code&gt; classifies each instruction on your machine and analyzes it on the Reporails API, and neither step is a Claude session, so it spends none of your Claude tokens and gives the same files the same findings on every run. A spec outside the usual agent files gets checked when you name it (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ails check -v specs/migrations.md
  ┌─ Files (1)  1 directive · 67% prose
  │ specs/migrations.md  1 dir · 67% prose
  │   migrations.md
  │     L3    Hedged language ('should', 'consider', 'migh…  CORE:C:0043
  │     L3    No instruction on this topic is strong enoug…  CORE:C:0052
  │
  └─ 2 findings

  Quality   4.3 / 10  ▓▓▓▓▓▓▓▓▓░░░░░░░░░░░  (1.8s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ails&lt;/code&gt; reads one directive in the vague spec and files the rest as prose. &lt;code&gt;CORE:C:0043&lt;/code&gt; says of the hedge that "the model may treat this as optional", which is what all three sessions did with "ideally". On Pro, the remedy for &lt;code&gt;CORE:C:0052&lt;/code&gt; reads "state it as a direct instruction and name the tool, file or command it already refers to". That's the named version, and it comes back clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ails check specs/migrations.md
  ✓  No findings.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;a href="https://reporails.com/pricing" rel="noopener noreferrer"&gt;Pro&lt;/a&gt;, &lt;code&gt;/reporails:ails heal&lt;/code&gt; in Claude Code rewrites the file and checks the rewrite. Because the result is the same on every run and costs no Claude tokens, it can also run on every push, with a score the build has to meet:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reporails/cli/action@0.6.1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.REPORAILS_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;min-score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And the organizing part
&lt;/h2&gt;

&lt;p&gt;The last piece also handed over a question: how you keep a rule set organized once the harness stops doing it for you. One part of that is the rename it described. The demo has a path-scoped rule in &lt;code&gt;.claude/rules/docs.md&lt;/code&gt; for everything under &lt;code&gt;docs/&lt;/code&gt;. Rename &lt;code&gt;docs/&lt;/code&gt; to &lt;code&gt;doc/&lt;/code&gt; and the rule never loads again, and nothing in the log tells you, because a rule that doesn't load doesn't log. The same check reads &lt;code&gt;paths:&lt;/code&gt; against the files that exist (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git mv docs doc &amp;amp;&amp;amp; ails check
  ┌─ Rules (1)  2 directive · 0% prose
  │ docs  2 dir · 0% prose
  │   ℹ Path glob `docs/**` matches no file in the p…  CLAUDE:S:0012
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where each rule should live, and what happens when you move a rule set into skills, is the next piece.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7kw5shjpjdqfi9og8yh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7kw5shjpjdqfi9og8yh.png" alt="Two instruments side by side. The bus log answers when something loaded: specs/migrations.md delivered once, on the prompt that started the migration work. The file check answers whether what loaded names what it means: the vague spec flagged as hedged with nothing strong enough to beat the model's default, the named spec clean." width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So when Claude Code isn't following an instruction, read the instruction before you blame the session. Mine said "ideally" and never said where the release comes from, and every session read it as a release still to come. &lt;code&gt;ails check&lt;/code&gt; points at that sentence from the spec file alone, without watching the agent run.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;npx @reporails/cli check&lt;/code&gt; in your project, and name the spec files your bus delivers. It's free and needs no account. &lt;a href="https://reporails.com/pricing" rel="noopener noreferrer"&gt;Pro&lt;/a&gt; adds the remedies and the workflow your coding agent runs to fix the files. Subscribe in October and enter the code &lt;code&gt;HACKTOBER&lt;/code&gt; at checkout.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt;, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface you wrote down and tells you, with measured evidence, which instructions couple to behavior and which are text the model can ignore. It does not run your model; it measures the files.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>performance</category>
    </item>
    <item>
      <title>Claude Code mods: but does it run Doom?</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Mon, 05 Oct 2026 15:43:43 +0000</pubDate>
      <link>https://dev.to/reporails/claude-code-mods-but-does-it-run-doom-2mma</link>
      <guid>https://dev.to/reporails/claude-code-mods-but-does-it-run-doom-2mma</guid>
      <description>&lt;p&gt;Yes ... Yes it does.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/reporails/claude-code-mods-minesweeper-and-testkit-3067"&gt;Minesweeper&lt;/a&gt; was the warm-up. A pane that can draw pixels and take the keyboard raises the only question the internet ever asks of a new screen, so the second game in the arcade is Doom: Freedoom on the doomgeneric engine, in a pane beside the transcript, while Claude works.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/doom&lt;/code&gt; opens the pane and starts the game. In kitty the screen is a real picture at Doom's own 320×200 (Ghostty speaks the same protocol, but I haven't tried it); in any other terminal it is drawn in quadrant block characters, four pixels a cell. Walk with &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;s&lt;/code&gt;, turn with &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;d&lt;/code&gt; or by dragging sideways on the game, fire with space or the right mouse button.&lt;/p&gt;

&lt;p&gt;Getting a picture on screen took an afternoon. The controls took the rest of the week, because a terminal never tells you that a key was let go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doom runs in a process of its own
&lt;/h2&gt;

&lt;p&gt;A mod's hooks run in a sandbox: no Node, no DOM, no &lt;code&gt;require&lt;/code&gt;, and deliberately no &lt;code&gt;WebAssembly&lt;/code&gt;. The typings say what to do instead: run the heavy thing as a program of its own. So the game is doomgeneric compiled to a native binary, and the mod starts it with &lt;code&gt;$.process.spawn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;engineArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;DOOM_CLAUDE_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A spawned child lives as long as the mod reads its output and ends when the mod unloads, so there is no daemon to clean up. The engine prints &lt;code&gt;doom-claude listening&lt;/code&gt; once it is ready, and from then on the mod talks to it with &lt;code&gt;$.http.fetch&lt;/code&gt;: over a Unix socket on Linux and macOS (&lt;code&gt;socketPath&lt;/code&gt;), and over 127.0.0.1 with a random 64-digit token on Windows, where every request without the token gets a 403.&lt;/p&gt;

&lt;p&gt;There is no compiler on the player's machine to count on, so the arcade ships six prebuilt engines, cross-compiled with &lt;code&gt;zig cc&lt;/code&gt; from one Linux box: Linux, macOS and Windows, each on x86_64 and arm64. Honest status: only the Linux x86_64 one has run. The macOS and Windows builds are compiled and checked (&lt;code&gt;file&lt;/code&gt; says Mach-O and PE32+, the arm64 Mach-O carries its ad-hoc signature), and nobody has started one yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pixels that never pass through Claude Code
&lt;/h2&gt;

&lt;p&gt;In kitty and Ghostty, Claude Code's &lt;code&gt;Image&lt;/code&gt; element can take a file instead of bytes. The engine writes each frame as raw RGB to a file beside its socket (written aside, then renamed over the old one, so it is never read half-written), and the mod points the &lt;code&gt;Image&lt;/code&gt; at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;imageSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ImageSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IMAGE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IMAGE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code hands kitty the path (&lt;code&gt;a=T,…,t=f&lt;/code&gt; in the graphics protocol), kitty reads the pixels itself, and a new &lt;code&gt;generation&lt;/code&gt; makes it read the file again. Not one pixel goes through Claude Code.&lt;/p&gt;

&lt;p&gt;Every other terminal gets a &lt;code&gt;Raster&lt;/code&gt;, and a &lt;code&gt;Raster&lt;/code&gt; has opinions. It rounds every colour to 4 bits a channel. It paints at most 1024 colour pairs and snaps the rest, which speckles a photo-like picture. And it leaves blank cells at the end of a row undrawn, so a dark corridor came out with black holes in it. The engine pre-encodes each frame into cells: a 32-colour palette per scene (32 × 32 is 1024), each cell the quadrant glyph and the two colours that fit its four pixels best, a cell reusing its left neighbour's colours when they fit nearly as well, and no blank cells at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code stops drawing when nothing moves
&lt;/h2&gt;

&lt;p&gt;The first live run froze for 300 ms at a time. &lt;code&gt;$.ui.blit&lt;/code&gt; repaints a &lt;code&gt;Raster&lt;/code&gt; at Claude Code's next frame, and with nothing else on screen moving, Claude Code draws only about three frames a second. The game was sending 30.&lt;/p&gt;

&lt;p&gt;The fix is a heartbeat: the red strip under the game is a &lt;code&gt;Client&lt;/code&gt;, and it redraws itself every 30 ms, swapping one blank character for another. That keeps Claude Code drawing, and it costs far less than redrawing the whole pane from the hooks module. One more trap on the way: a blit resolves when it has been painted, not when it has been taken, so the frame loop must not await it before fetching the next frame.&lt;/p&gt;

&lt;p&gt;On a machine with other work running (load 5–6 on 8 cores), the picture changed 35–40 times a second in a plain terminal and 31–32 times a second in kitty. The block picture costs Claude Code about a full core; the kitty picture about a third of one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The keyboard that never lets go
&lt;/h2&gt;

&lt;p&gt;Doom wants to know when a key goes down and when it comes up. A terminal reports the press, then, after its repeat delay, the same press again every 30 ms or so, and never the release. Claude Code turns on kitty's keyboard protocol with flags 5 only, so a mod gets no release events and can't even tell a repeat from a new press. The terminal also repeats only the newest key: hold &lt;code&gt;w&lt;/code&gt;, press &lt;code&gt;a&lt;/code&gt;, and &lt;code&gt;w&lt;/code&gt; goes quiet whether your finger is still on it or not.&lt;/p&gt;

&lt;p&gt;So every terminal Doom fakes the release, and my first attempt was a guess. A turn key held for 220 ms, so a tap would be a small turn. Then the terminal's first repeat came at 500 ms, and a held turn key stopped dead for 308 ms before it started turning for real. I measured it by reading the player's facing out of the engine every 10 ms. Then I patched it, and patched the patch.&lt;/p&gt;

&lt;p&gt;What fixed it was reading how the others do it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;port&lt;/th&gt;
&lt;th&gt;fakes the release how&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ludocode/doom-cli" rel="noopener noreferrer"&gt;doom-cli&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;learns the terminal's repeat delay and rate per key; a fresh press holds until the first repeat is due, a repeat until the next is due&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/jarrodwatts/intermission" rel="noopener noreferrer"&gt;intermission&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;550 ms after a fresh press, 120 ms while repeating; turning is mouse-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ChaseWNorton/claude-doom" rel="noopener noreferrer"&gt;claude-doom&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;a 160 ms pulse per press&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/eduresser/doom-braille" rel="noopener noreferrer"&gt;doom-braille&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;released after 100 ms with no bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;doom-cli's model is the one that holds up, so the engine now does the same. A movement key holds until its next repeat is due. The repeat delay is learnt from the gap between a press and its first repeat (the median of the last five, capped at 600 ms), so it fits GNOME's 500 ms as well as anyone's faster setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="nf"&gt;freshHold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;isMovement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;s_RepeatDelay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;DELAY_MARGIN_MS&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OTHER_HOLD_MS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That has a cost doom-cli's own source admits in a TODO: a tap now lasts as long as the repeat delay, so one tap of &lt;code&gt;d&lt;/code&gt; turned 61.5°. The TODO also names the fix: &lt;em&gt;"just turn more slowly outside of state repeat? so it's still possible to do some precision aiming"&lt;/em&gt;. So a turn key in play doesn't press Doom's arrow key at all. It turns through Doom's mouse, slowly until the terminal starts repeating it, then at the arrow keys' full speed, with the same half-speed first six tics Doom gives a held key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isRepeating&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s_KeyTurnTics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;TAP_TURN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_KeyTurnTics&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;SLOW_TURN_TICS&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;KEY_TURN&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KEY_TURN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tap of &lt;code&gt;d&lt;/code&gt; now turns 10.5°, about what a quick tap gives in Doom with a real keyboard, and a held &lt;code&gt;d&lt;/code&gt; never stops for longer than 28 ms. In a menu, a pause or the title demo, the arrows stay arrow keys, because menu sliders want them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mouse is the only exact hold
&lt;/h2&gt;

&lt;p&gt;A pointer is the one input that does report its release. So dragging sideways on the game turns, and letting go stops at once. It only turns: walking stays on &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;s&lt;/code&gt;, and the keys and the mouse work together, so you can walk and turn at the same time, which the keyboard alone can't do.&lt;/p&gt;

&lt;p&gt;The first version of the stick walked too, and turned up to 2.5 times faster than the keys the further you dragged. Now any sideways drag past a small dead zone turns exactly as fast as &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;d&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stickOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pad&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Stick&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;turn&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="na"&gt;forward&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="na"&gt;buttons&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isHeld&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;turn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;STICK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dead&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;STICK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buttons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isFiring&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;STICK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fire&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isHeld&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isStrafing&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;STICK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strafe&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;forward&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="nx"&gt;buttons&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;80 is Doom's mouse turning at 640 units a tic, the same as a held arrow key, and the engine gives a drag the same half-speed start. A drag and a held &lt;code&gt;d&lt;/code&gt; stayed within a tic of each other at every point I measured: 51.0° against 52.8° at half a second, 114.3° against 112.5° at one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escape belongs to the prompt
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Client&lt;/code&gt; gets the keyboard only after a click, and Escape always hands it back to Claude Code's prompt. No mod and no keybinding can take Escape: it never reaches the mod at all. So the menu is &lt;code&gt;m&lt;/code&gt; or backspace. Before the first click, space and the digits land in the prompt too.&lt;/p&gt;

&lt;p&gt;While you play, then, the prompt is Doom's. A &lt;code&gt;prompt.edit&lt;/code&gt; hook takes game keys out of the prompt and hands them to the game, and drops the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPromptOurs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputText&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;codes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doomKeyOf&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gameInputAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queuedKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promptCheckAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;PROMPT_CHECK_MS&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cursor&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Answering &lt;code&gt;{ text, cursor }&lt;/code&gt; without calling &lt;code&gt;next&lt;/code&gt; consumes the key. The hook decides without a single &lt;code&gt;$&lt;/code&gt; call, because Claude Code sometimes lets the key through anyway when the hook is slow. Type &lt;code&gt;/&lt;/code&gt; and the prompt is yours again at once, ten seconds without a game key gives it back too, and a draft you had typed before is never touched.&lt;/p&gt;

&lt;p&gt;This makes Doom different from the Minesweeper in one way that matters: it hooks the prompt. &lt;code&gt;claude plugin validate&lt;/code&gt; lists it before any of the mod's code runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ ./register.ts hooks: session.start, command.run{command=doom}, ui.render{component=Pane}, ui.message, prompt.edit, ui.close, session.end
❯ ./register.ts calls: $.clock.after (via fitPane, focusSoon, watchEngine), $.clock.every (via startPulling), $.command.register, $.env.get (via readEnv), $.fs.exists (via findEngine), $.fs.write (via watchEngine), $.http.fetch (via pullFrame, sendKeys, sendStick, stopEngine), $.process.run (via findEngine, startEngine), $.process.spawn (via startEngine), $.prompt.fill (via clearLeakedPrompt), $.prompt.read (via clearLeakedPrompt), $.session.surfaces, $.ui.blit (via paint), $.ui.close, $.ui.invalidate, $.ui.open (via fitPane, openPane), $.ui.resolve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads nothing the model sees and adds nothing to it, and its only network traffic is to its own engine on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the test kit missed this time
&lt;/h2&gt;

&lt;p&gt;The mod has 20 tests under &lt;code&gt;claude plugin test&lt;/code&gt;, typechecked under the same strict config as the code. A review still found two bugs in them that a player would have hit in the first minute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A held drag stopped after 1.5 s.&lt;/strong&gt; The engine lets go of a stick it hasn't heard about for 1.5 s, in case the mod died mid-drag. The strip re-sends a held stick on its heartbeat, but the layer over the game, where you actually drag, had no heartbeat. Hold still while turning or firing, and you stopped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A restarted engine replayed old keys.&lt;/strong&gt; Each pane numbers its keys so a post that replaces an undelivered one loses nothing. Quit Doom from its own menu and run &lt;code&gt;/doom&lt;/code&gt; again, and the hooks module forgot the numbers while the pane kept counting: the new engine got the last 24 keys again, menu and quit included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first one is a lesson about the kit. The new test for it failed with the bug in place only once it stepped the frame clock one frame at a time: the kit delivers one post per &lt;code&gt;ui.advance&lt;/code&gt;, so a two-second advance collapsed six re-sends into one and passed anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play it
&lt;/h2&gt;

&lt;p&gt;In a Claude Code session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add reporails/arcade
/plugin install doom@reporails-arcade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then start Claude Code in the fullscreen layout, which the mouse needs, and run &lt;code&gt;/doom&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;CLAUDE_CODE_NO_FLICKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mods load by default from Claude Code 2.1.287; on 2.1.285 and 2.1.286, add &lt;code&gt;CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1&lt;/code&gt;. Run it in kitty for the real picture (Ghostty should work the same, untested); every other terminal gets the blocks, and so does Windows, where no terminal the mod detects speaks kitty's picture protocol. It has been played on Linux. If you run it on a Mac or on Windows, tell me how it went.&lt;/p&gt;

&lt;p&gt;The mod's code is MIT, in &lt;code&gt;reporails/arcade&lt;/code&gt; on GitHub. The engine is doomgeneric, GPL-2.0-or-later, and the game data is Freedoom, BSD. "Doom" is id Software's name; this plays Freedoom and contains nothing of id's.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>javascript</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Claude Code mods: Minesweeper and testkit</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Sat, 03 Oct 2026 17:30:29 +0000</pubDate>
      <link>https://dev.to/reporails/claude-code-mods-minesweeper-and-testkit-3067</link>
      <guid>https://dev.to/reporails/claude-code-mods-minesweeper-and-testkit-3067</guid>
      <description>&lt;p&gt;My own harness, the tooling I run around Claude Code, comes with a custom trust score. That trust score decides whether I can leave the agent alone yet, without supervision or if it's in my best interest to babysit it. As you see in the header image, it's babysitting era, so I built a Minesweeper to kill time between turns.&lt;/p&gt;

&lt;p&gt;From Claude Code 2.1.287, mods load by default. A mod is a plugin whose hooks are JavaScript or TypeScript functions running inside Claude Code instead of shell commands, and it can open and draw its own panes. Minefield is Minesweeper as a mod: &lt;code&gt;/mines&lt;/code&gt; opens a pane beside the transcript, and you clear the board while Claude works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjn0epjfvyyv7dumia7jl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjn0epjfvyyv7dumia7jl.png" alt="playing minesweeper" width="670" height="842"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is one 9×9 board with 10 mines, a game for the few minutes Claude works. The first reveal is always safe and opens an area, clicking a fully flagged number opens the cells around it, and the best time is kept between sessions. The orange face follows your cursor. Stop for six seconds and it pretends to read the transcript beside it, eyes half shut, line by line. It only looks; the mod sees nothing of the transcript. It winks at flags and puts on sunglasses when you win. This was the most fun part to implement.&lt;/p&gt;

&lt;p&gt;It ships with 31 tests that run through &lt;code&gt;claude plugin test&lt;/code&gt;, and CI runs them on two pinned Claude Code builds. Four of its bugs passed the test kit anyway and only showed up in a real session: the pane couldn't take the keyboard, the hotkeys did nothing, the face wrapped, and the mouse wasn't there.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;claude plugin test&lt;/code&gt;, nearly every engine call your mod makes (&lt;code&gt;ui.open&lt;/code&gt;, &lt;code&gt;ui.panes&lt;/code&gt;, &lt;code&gt;store.get&lt;/code&gt;) is answered by an &lt;code&gt;on(...)&lt;/code&gt; stub you write in the test file; the kit answers a few itself, such as &lt;code&gt;$.ui.invalidate&lt;/code&gt;. That is why it runs fast and without credentials. It also means the host in your tests behaves exactly as well as your doubles do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pane and the keyboard
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/mines&lt;/code&gt; opened the pane, and the keys kept going to the prompt.&lt;/p&gt;

&lt;p&gt;Seen in live sessions on 2.1.285: a pane takes the keyboard only over an empty prompt. Anthropic's docs list &lt;code&gt;focus: true&lt;/code&gt; from a command as one way a pane gets the keyboard, but while &lt;code&gt;command.run&lt;/code&gt; runs, the command's own text is still in the prompt, and on 2.1.285 the request was refused. The pane opens and the keyboard stays where it was.&lt;/p&gt;

&lt;p&gt;In the tests, &lt;code&gt;ui.open&lt;/code&gt; is answered by my own double, and my double says yes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;opened&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;closeOnEscape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closeOnEscape&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isPlaced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&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 fix is to open the pane, then ask for the keyboard again once &lt;code&gt;/mines&lt;/code&gt; has returned. &lt;code&gt;register.ts&lt;/code&gt; waits 250 ms, then 500, then 1000 between tries (&lt;code&gt;FOCUS_RETRIES_MS&lt;/code&gt;), three tries inside two seconds, and every retry first asks &lt;code&gt;$.ui.panes()&lt;/code&gt; whether the pane is still open and still without the keyboard. If the pane is gone, the person closed it, and a retry must not bring it back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ask for the keyboard again once /mines has returned, while the pane is open&lt;/span&gt;
&lt;span class="c1"&gt;// and still without it; never reopen a pane the person has closed.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;focusSoon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EngineInterface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FOCUS_RETRIES_MS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&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="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;refocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No timer; a click on the pane gives it the keyboard.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EngineInterface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pane&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;panes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;PANE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pane&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;pane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;openPane&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;focusSoon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The panes cannot be listed; a click on the pane gives it the keyboard.&lt;/span&gt;
  &lt;span class="p"&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 retry logic is testable, with &lt;code&gt;mock.clock&lt;/code&gt; standing in for &lt;code&gt;$.clock&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a pane the person closed is not reopened by the focus retries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;opened&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Opened&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;opened&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pane&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SESSION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;advance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opened&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What no test here can check is the refusal itself. &lt;code&gt;isFocused&lt;/code&gt; in that test is whatever my double says it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hotkeys that did nothing
&lt;/h2&gt;

&lt;p&gt;A pane has two places to draw from. The hooks module (&lt;code&gt;register.ts&lt;/code&gt;) answers the &lt;code&gt;ui.render&lt;/code&gt; hook and returns elements: boxes, text, buttons. A &lt;code&gt;Client&lt;/code&gt; is a surface module (&lt;code&gt;board.ts&lt;/code&gt;) with its own state, a frame timer, and pointer and key handlers, which is where a game wants to live. So the control buttons went in the board, next to the cells they act on, each with a one-letter &lt;code&gt;hotkey&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of the hotkeys fired. A &lt;code&gt;Button&lt;/code&gt;'s &lt;code&gt;hotkey&lt;/code&gt; fires only for buttons the hooks module draws, never for buttons a &lt;code&gt;Client&lt;/code&gt; draws.&lt;/p&gt;

&lt;p&gt;The tests had no way to notice. &lt;code&gt;ui.press({ key: 'flag' })&lt;/code&gt; presses a button by its element key (&lt;code&gt;'flag'&lt;/code&gt;, where the hotkey is &lt;code&gt;f&lt;/code&gt;), so a press in a test never goes through the hotkey at all.&lt;/p&gt;

&lt;p&gt;The fix moved the buttons into &lt;code&gt;register.ts&lt;/code&gt;. That raised the next problem: the buttons now live in the hooks module, the game lives in the board, and the board learns about presses only through its props. Several presses can arrive between two redraws of the board, so handing over only the latest move drops the others. Each press gets a number instead, and the hooks module keeps the last 32:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Moves kept for the board, so presses faster than its redraws all arrive.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_ACTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MoveType&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&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="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;}].&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;MAX_ACTS&lt;/span&gt;&lt;span class="p"&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 board plays every move newer than the last one it played, in order, once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every move newer than the last one played, in order. Presses can outrun&lt;/span&gt;
&lt;span class="c1"&gt;// the board's redraws, so one redraw may bring several. Numbers lower than&lt;/span&gt;
&lt;span class="c1"&gt;// the last one played mean the hooks module started counting again, as it&lt;/span&gt;
&lt;span class="c1"&gt;// does when the plugin reloads, so every move it lists is new. The same game&lt;/span&gt;
&lt;span class="c1"&gt;// back when there is nothing new.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyActs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;acts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;Act&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;Game&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;acts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&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="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acted&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;acted&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="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;move&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;acts&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;move&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&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;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;move&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;acted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;move&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mounted test can't catch the race this guards against. The kit redraws after every press, so it never coalesces presses, and a mounted test of the queue passes even with the bug in place. So the race is tested as a pure function, with the presses that land in one redraw written out by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;every move newer than the last one played is played, in order, once&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;newGame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Three presses that arrive in one redraw, the first one already played&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;acts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;n&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;n&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;down&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;n&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reveal&lt;/span&gt;&lt;span class="dl"&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;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;played&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyActs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;acted&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="nx"&gt;acts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;played&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;played&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acted&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;played&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Nothing newer: the same game back&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;applyActs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;played&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;acts&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;played&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// After a reload the hooks module counts its moves from 1 again: every move it lists is new&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reloaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyActs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;played&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;acted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;n&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}])&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;reloaded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reloaded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acted&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;x&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="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The face that wrapped
&lt;/h2&gt;

&lt;p&gt;The face is four rows of text, nine columns wide (&lt;code&gt;FACE_WIDTH = 9&lt;/code&gt;), drawn in Claude Code's own orange: the theme's &lt;code&gt;claude&lt;/code&gt; colour, so it follows your theme. The status sits beside it in a row of boxes.&lt;/p&gt;

&lt;p&gt;A row of boxes shrinks every box to fit. In a real pane the face's column came out one column short, and each of its nine-column rows wrapped in two. Eight rows of orange, and a face that looked like something had sat on it.&lt;/p&gt;

&lt;p&gt;The tests find the face by its text (&lt;code&gt;eyes('  ●   ●  ')&lt;/code&gt;, a &lt;code&gt;ui.find&lt;/code&gt; for that &lt;code&gt;Text&lt;/code&gt; in the board), and the text was right. The strings were nine columns wide; the column holding them was eight.&lt;/p&gt;

&lt;p&gt;The fix is a fixed &lt;code&gt;width&lt;/code&gt; plus &lt;code&gt;flexShrink: 0&lt;/code&gt; on the face's column. The status column beside it shrinks and truncates (&lt;code&gt;wrap: 'truncate-end'&lt;/code&gt;), so the long status is the part that gets cut, and the face's own rows truncate rather than wrap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// The face's column keeps its width, so long text beside it is cut and the&lt;/span&gt;
  &lt;span class="c1"&gt;// face is not.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;face&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;faceRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;truncate-end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;faceColumn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FACE_WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flexShrink&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="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;face&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The board's own rows carry &lt;code&gt;flexShrink: 0&lt;/code&gt; for the same reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mouse that wasn't there
&lt;/h2&gt;

&lt;p&gt;On Claude Code's main screen, the pane gets no clicks. Claude Code asks the terminal for pointer reporting only in the fullscreen layout: &lt;code&gt;CLAUDE_CODE_NO_FLICKER=1&lt;/code&gt; at start, or &lt;code&gt;/tui fullscreen&lt;/code&gt; in a session. On the main screen it asks for none at all.&lt;/p&gt;

&lt;p&gt;In the kit, &lt;code&gt;ui.pointer({ type: 'down', ... })&lt;/code&gt; hands the board a click directly. In a real terminal the click has to be asked for first. To see whether Claude Code asked, &lt;code&gt;tmux display-message -p '#{mouse_any_flag}'&lt;/code&gt; prints the terminal's mouse flag.&lt;/p&gt;

&lt;p&gt;So the game is keyboard-first, and every action has a key. The letter keys (&lt;code&gt;n&lt;/code&gt; new, &lt;code&gt;r&lt;/code&gt; reveal, &lt;code&gt;f&lt;/code&gt; flag, &lt;code&gt;w&lt;/code&gt; &lt;code&gt;a&lt;/code&gt; &lt;code&gt;s&lt;/code&gt; &lt;code&gt;d&lt;/code&gt; to move) are the hotkeys of the control buttons from the previous bug, drawn by the hooks module, so they work as soon as the pane has the keyboard, with no click. Arrow keys, &lt;code&gt;h&lt;/code&gt; &lt;code&gt;j&lt;/code&gt; &lt;code&gt;k&lt;/code&gt; &lt;code&gt;l&lt;/code&gt;, space and return reach the board through &lt;code&gt;onKey&lt;/code&gt;, which a &lt;code&gt;Client&lt;/code&gt; gets only after a click gives it the keyboard. Escape hands the keys back to the prompt.&lt;/p&gt;

&lt;p&gt;The line under the buttons says which screen you are on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One line under the controls, for the screen the pane is on: the mouse&lt;/span&gt;
&lt;span class="c1"&gt;// exists only in the fullscreen layout, and a pane without the keyboard&lt;/span&gt;
&lt;span class="c1"&gt;// takes them back with ctrl+x tab.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hintFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isFullscreen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ctrl+x tab gives the pane the keys again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isFullscreen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Click reveals, right-click flags; click a fully flagged number to open around it.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Keys only on this screen: /tui fullscreen adds the mouse.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The loop that found them
&lt;/h2&gt;

&lt;p&gt;So the test kit gets the same treatment as the agent: I don't leave it alone either. Before a mod counts as done, it runs in a real interactive session, driven by a script. tmux does the driving: start Claude Code detached at a fixed size, type the command, send mouse escapes for clicks, and read the screen back as text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux new-session &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; mod &lt;span class="nt"&gt;-x&lt;/span&gt; 160 &lt;span class="nt"&gt;-y&lt;/span&gt; 50 &lt;span class="s2"&gt;"CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 CLAUDE_CODE_NO_FLICKER=1 claude --plugin-dir &amp;lt;mod&amp;gt;"&lt;/span&gt;
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; mod &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'/&amp;lt;command&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; mod Enter
tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; mod &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s1"&gt;[&amp;lt;0;COL;ROWM'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; mod &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s1"&gt;[&amp;lt;0;COL;ROWm'&lt;/span&gt;   &lt;span class="c"&gt;# left click (SGR); button 2 is right&lt;/span&gt;
tmux capture-pane &lt;span class="nt"&gt;-t&lt;/span&gt; mod &lt;span class="nt"&gt;-p&lt;/span&gt;        &lt;span class="c"&gt;# the screen as text; -e keeps the colours&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run it from an empty trusted folder, so no project hooks fire.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1&lt;/code&gt; is the switch mods need on 2.1.285 and 2.1.286. From 2.1.287 they load by default.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\e[&amp;lt;0;COL;ROWM&lt;/code&gt; is an SGR mouse press at that column and row, and the lowercase &lt;code&gt;m&lt;/code&gt; is its release. Swap the &lt;code&gt;0&lt;/code&gt; for a &lt;code&gt;2&lt;/code&gt; to right-click.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;capture-pane -p&lt;/code&gt; prints the screen as plain text, which means Claude can read its own UI back and check it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule for the loop: never script a win. The best time lives in the plugin's &lt;code&gt;$.store&lt;/code&gt;, which survives sessions, so a scripted win writes a record to the real store, and my best time would belong to tmux. The live sessions played through a loss. The wins, and the stored best time, are covered by tests with a store double.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin the build, and show what your mod can see
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;claude -p "/plugin-types"&lt;/code&gt; writes &lt;code&gt;.claude/types/claude-code.d.ts&lt;/code&gt; for the installed build, so one pinned Claude Code build serves the typings, the engine and the test kit, and the three can't disagree. Typecheck against those, and leave the typings on the &lt;code&gt;main&lt;/code&gt; branch of &lt;code&gt;anthropics/claude-code&lt;/code&gt; alone: the two drift. On &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;ui.open&lt;/code&gt; answers &lt;code&gt;void&lt;/code&gt;. On 2.1.285 it answers &lt;code&gt;{ isPlaced }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The typings stay out of git. Claude Code ships under "All rights reserved" (its npm &lt;code&gt;LICENSE.md&lt;/code&gt;), and a copy in an MIT repo would breach that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Claude Code writes these: `/plugin-types` into .claude/types/, and loading a
# mod into its own folder. Each build writes its own, so none is committed.
.claude/types/
*/.claude-plugin/types/
*/tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/plugin-types&lt;/code&gt; runs without a login on 2.1.285, behind the switch. From 2.1.287, &lt;code&gt;claude -p&lt;/code&gt; asks for a login. So CI runs two jobs on every push to &lt;code&gt;main&lt;/code&gt; and every pull request. The first writes the typings, typechecks, validates and tests on 2.1.285:&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="c1"&gt;# The typecheck, on the one build that writes its mod typings without a&lt;/span&gt;
  &lt;span class="c1"&gt;# login: one build for the typings, the engine and the test kit, so they&lt;/span&gt;
  &lt;span class="c1"&gt;# cannot disagree. Mods are early access on it, behind the switch.&lt;/span&gt;
  &lt;span class="na"&gt;typecheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;CLAUDE_CODE_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.1.285&lt;/span&gt;
      &lt;span class="na"&gt;CLAUDE_CODE_ENABLE_FUNCTION_HOOKS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt;
      &lt;span class="na"&gt;DISABLE_AUTOUPDATER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Claude Code&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Write the mod typings for that build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude -p "/plugin-types" &amp;lt; /dev/null&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Typecheck&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx -y -p typescript@5.9.3 tsc -p .&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate and test each game&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;for manifest in */.claude-plugin/plugin.json; do&lt;/span&gt;
            &lt;span class="s"&gt;game="${manifest%/.claude-plugin/plugin.json}"&lt;/span&gt;
            &lt;span class="s"&gt;claude plugin validate --strict "$game"&lt;/span&gt;
            &lt;span class="s"&gt;claude plugin test "$game"&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second installs 2.1.288, where mods are on by default, validates the marketplace, and runs &lt;code&gt;claude plugin validate --strict&lt;/code&gt; and &lt;code&gt;claude plugin test&lt;/code&gt; again, on the kind of build players run.&lt;/p&gt;

&lt;p&gt;Then there's what the mod can see. The host reads &lt;code&gt;on(...)&lt;/code&gt; and &lt;code&gt;$.noun.method(...)&lt;/code&gt; from the hooks module's source before it runs any of it, which is why &lt;code&gt;register.ts&lt;/code&gt; spells them literally and keeps every helper that takes &lt;code&gt;$&lt;/code&gt; at the top level. &lt;code&gt;claude plugin validate ./minefield&lt;/code&gt; prints what it read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ ./register.ts hooks: session.start, command.run{command=mines}, ui.render{component=Pane}, ui.message
❯ ./register.ts calls: $.clock.after (via focusSoon), $.clock.now (via deal), $.command.register, $.session.surfaces, $.store.get, $.store.set (via recordWin), $.ui.invalidate, $.ui.open (via openPane), $.ui.panes (via refocus), $.ui.resolve
❯ ./register.ts surface modules: hooks/board.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four hooks: session start, its own &lt;code&gt;/mines&lt;/code&gt; command, pane drawing (it draws its own and passes every other pane on untouched), and messages from its board. No prompt, tool or attachment hook, and no network call. The "By Reporails" at the end of the status line is a &lt;code&gt;Link&lt;/code&gt; element; your terminal opens it, and the mod makes no request.&lt;/p&gt;

&lt;p&gt;When I pulled the awesome-claude-code-mods data on 3 October 2026, 479 of its 1,018 mods were listed as seeing every prompt or every tool call. Plenty need to: the tool-call counter on Anthropic's own mods overview has to see every tool call. A Minesweeper has no use for either. The count, if you want to re-run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://raw.githubusercontent.com/karanb192/awesome-claude-code-mods/main/data/mods.json | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys,json;m=[x for x in json.load(sys.stdin)['mods'] if x['kind']=='mod'];print(sum(any(k in ' '.join(x['sees']) for k in ('every prompt','every tool call')) for x in m),len(m))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus: making a 9×9 board fill a docked pane
&lt;/h2&gt;

&lt;p&gt;In a wide fullscreen layout the pane docks beside the transcript and opens floor to ceiling, at the 60 columns it asks for. A 9×9 board at one row a cell uses a corner of that. &lt;code&gt;fitFor&lt;/code&gt; picks the biggest cell the room holds, from the pane's columns and rows, biggest cells first and the face under the board before beside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every fit, biggest cells first; under the board before beside it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FITS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;Fit&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;SCALES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&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="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&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="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&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="na"&gt;height&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="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&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="na"&gt;height&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="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&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="na"&gt;height&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="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&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="na"&gt;height&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="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;// The biggest fit the room holds; with the room unknown, or too small for&lt;/span&gt;
&lt;span class="c1"&gt;// any, a row a cell, two columns wide where the room has them, the face under&lt;/span&gt;
&lt;span class="c1"&gt;// the board.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nx"&gt;Fit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FITS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;columnsFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;rowsFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cellWidthFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;height&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="nx"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SCALES&lt;/code&gt; is &lt;code&gt;[3, 2]&lt;/code&gt;, so the big fits are cells six columns by three rows, or four by two. A terminal cell is about twice as tall as it is wide (the reason a board at one row a cell already spends two or three columns a cell where it can), so &lt;code&gt;2k&lt;/code&gt; columns by &lt;code&gt;k&lt;/code&gt; rows is square on screen. The gaps need the same care. Each tile is &lt;code&gt;width - 1&lt;/code&gt; columns of colour with a one-column gap after it, and its last row is an upper half block, &lt;code&gt;▀&lt;/code&gt;, so the gap between rows is half a row tall, the same size as the one-column gap between tiles. At the six-by-three fit that's five columns by two and a half rows of colour, square again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;across&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;glyphRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;glyphAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;across&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tile&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;tiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tileOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Run&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;glyph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fill&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;tiles&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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="nf"&gt;pushRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;▀&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;across&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fill&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;glyphRow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;pushRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;glyphAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;glyph&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;across&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;glyphAt&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="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fill&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;pushRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;across&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fill&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;pushRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;fitFor&lt;/code&gt; is a pure function, the sizes are tested without a pane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// A docked pane: square tiles three rows tall, or two, where they fit&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&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="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&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="na"&gt;height&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="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above the prompt, rows are what's scarce, so the pane asks for the rows of the most compact fit the terminal's width holds: one row a cell, a hidden tile drawn as a short box (&lt;code&gt;▆▆&lt;/code&gt;), and the face and the status beside the board where they fit there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play it
&lt;/h2&gt;

&lt;p&gt;In a Claude Code session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add reporails/arcade
/plugin install minefield@reporails-arcade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;/mines&lt;/code&gt;. It needs Claude Code 2.1.287 or later. For the docked pane and the mouse, start Claude Code with &lt;code&gt;CLAUDE_CODE_NO_FLICKER=1&lt;/code&gt;, or run &lt;code&gt;/tui fullscreen&lt;/code&gt;. If minefield doesn't load, run &lt;code&gt;claude plugin test&lt;/code&gt; in an empty folder: &lt;code&gt;no hooks module to load&lt;/code&gt; means mods load for you. &lt;code&gt;hooks modules are turned off here&lt;/code&gt; means a setting blocks them: &lt;code&gt;disableAllHooks&lt;/code&gt; in your settings, or your organization's policy. &lt;code&gt;hooks modules are turned off in this process&lt;/code&gt; means Anthropic has them off for your account, and no local setting changes that.&lt;/p&gt;

&lt;p&gt;The repo is MIT, &lt;code&gt;reporails/arcade&lt;/code&gt; on GitHub. Post your best time in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>javascript</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Progressive Disclosure: Claude Code Hooks, Events and Telemetry</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Wed, 30 Sep 2026 12:49:50 +0000</pubDate>
      <link>https://dev.to/reporails/progressive-disclosure-claude-code-hooks-events-and-telemetry-1fh8</link>
      <guid>https://dev.to/reporails/progressive-disclosure-claude-code-hooks-events-and-telemetry-1fh8</guid>
      <description>&lt;p&gt;The last piece (&lt;a href="https://reporails.com/articles/progressive-disclosure-shaping-claude-code-output" rel="noopener noreferrer"&gt;Progressive Disclosure: Shaping Claude Code's Output&lt;/a&gt;) ended on one small move. Every reply opens with its kind, &lt;code&gt;[status]&lt;/code&gt;, &lt;code&gt;[analysis]&lt;/code&gt; or &lt;code&gt;[narration]&lt;/code&gt;, and that token decides which reply-shape rules apply and which checks run when the reply ends. One &lt;code&gt;Stop&lt;/code&gt; hook read the token and did the rest.&lt;/p&gt;

&lt;p&gt;That's also where the approach runs out. &lt;code&gt;Stop&lt;/code&gt; fires once the reply is written, so a hook there can judge the reply and send it back, but anything it wants the agent to know arrives after the words are already on the screen. And the one script keeps growing: the length check, the kind check, a counter for how often the kind goes missing, and next a rule for the docs folder that has nothing to do with the end of a reply.&lt;/p&gt;

&lt;p&gt;The first piece in the series (&lt;a href="https://reporails.com/articles/progressive-disclosure-what-where-when-and-why" rel="noopener noreferrer"&gt;Progressive Disclosure: What, Where, When, and Why&lt;/a&gt;) left two promises open: when to move a rule off the page and onto an event, and how you'd know it loaded. This piece keeps both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different moments, different rules
&lt;/h2&gt;

&lt;p&gt;A rule set is several lists, one per moment of a turn, and Claude Code can run a script of yours, called a hook, at each of those moments (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A prompt arrives&lt;/strong&gt; (&lt;code&gt;UserPromptSubmit&lt;/code&gt;). Whatever a hook prints here goes in front of the model before it starts. That's the place for a rule about the work ahead: while you're on a database migration, every migration ships with a down step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tool is about to run&lt;/strong&gt; (&lt;code&gt;PreToolUse&lt;/code&gt;). A hook here sees which tool and which file. A documentation rule belongs on the write under &lt;code&gt;docs/&lt;/code&gt;, not on every prompt. What it adds lands next to the tool's result, so it shapes the edits after the first one. When even the first write has to follow the rule, the hook blocks the call and puts the rule in the reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reply ends&lt;/strong&gt; (&lt;code&gt;Stop&lt;/code&gt;). The reply is finished and can be judged, so this is where checks live. The right check depends on the kind of reply: a status report gets 180 words, a narration 150, an analysis no cap. A reply over its limit goes back once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk7tdeod6c5tjfju3t1ma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk7tdeod6c5tjfju3t1ma.png" alt="One turn, three moments. When a prompt arrives, UserPromptSubmit adds migration-safety before the model starts. When a write to docs/cache.md is about to run, PreToolUse adds docs-context-first, next to the tool result or by blocking the call. When the reply ends, Stop runs the reply-length check: 180 words for status, 150 for narration, no cap for analysis, and a reply over its limit is sent back once." width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checks split the same way. A migration's down step can't be checked until the file exists, so that check runs after the write, on a fourth moment, &lt;code&gt;PostToolUse&lt;/code&gt;. The length check waits for the finished reply and takes its limit from the reply's kind. Claude Code supplies the moments, and matching each rule and check to its moment is left to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one rule per hook stops scaling
&lt;/h2&gt;

&lt;p&gt;The obvious way to wire this up is one hook per rule: a script and a &lt;code&gt;settings.json&lt;/code&gt; entry each. Anthropic's &lt;a href="https://github.com/anthropics/claude-code/tree/main/plugins/hookify" rel="noopener noreferrer"&gt;hookify&lt;/a&gt; plugin tidies that up. Each rule is a markdown file naming an &lt;code&gt;event&lt;/code&gt;, a &lt;code&gt;pattern&lt;/code&gt; and an &lt;code&gt;action&lt;/code&gt;, and the plugin's own hooks check every rule file on each event. For a handful of rules that warn or block on what the harness can see, a command, a file, a prompt, it's the right start.&lt;/p&gt;

&lt;p&gt;Around rule twenty, or once a rule needs to know what the last reply said, the cracks show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each rule sees only the one event in front of it, and can match only what that event carries: a command, a file path, the prompt text.&lt;/li&gt;
&lt;li&gt;Nothing remembers anything between events. Sure, every hook gets a &lt;code&gt;transcript_path&lt;/code&gt;, so a rule could go digging for the last reply itself. Then every rule digs on its own, on every event, and you're back to twenty scripts parsing the same file.&lt;/li&gt;
&lt;li&gt;Nothing records what fired, so there's nothing to count.&lt;/li&gt;
&lt;li&gt;With a script per rule, a rule's condition also ends up in three places: a pattern in the script, a matcher in &lt;code&gt;settings.json&lt;/code&gt;, and the rule's text somewhere else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm8624de8i6bnsynb82ew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm8624de8i6bnsynb82ew.png" alt="Two panels. On the left, one rule per hook: settings.json points at five separate scripts, each with its own condition, and none of them knows what the others did. On the right, one entry point: settings.json points at a single bus.sh, which reads a rules folder and writes one log, with another agent's hooks coming in through a small adapter." width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is to send every event through one place. Here that place is an event bus, a single script Claude Code calls for every hook event you care about. It reads the event once, works out which rules asked for it, and answers the harness. Your &lt;code&gt;settings.json&lt;/code&gt; shrinks to one script, registered on each event:&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;"hooks"&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;"InstructionsLoaded"&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;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&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_PROJECT_DIR/.claude/bus/bus.sh"&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UserPromptSubmit"&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;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&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_PROJECT_DIR/.claude/bus/bus.sh"&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&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;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&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_PROJECT_DIR/.claude/bus/bus.sh"&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&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;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&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_PROJECT_DIR/.claude/bus/bus.sh"&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="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;h2&gt;
  
  
  Put telemetry on it first
&lt;/h2&gt;

&lt;p&gt;Before you move a single rule onto the bus, have it write down what happens. It's the cheapest step, and it changes what you can see.&lt;/p&gt;

&lt;p&gt;The agents that build Reporails open every reply with its kind and get checked on it when the reply ends. In the 13 days before I wrote this, across four of our repositories, they made 1,979 reply attempts, resends included, and 106 of them opened without a kind. That's about 1 in 19. All our end-of-reply checks together sent back 497 of those attempts, about 1 in 4. From the terminal you'd catch the odd resend scrolling past and never put a number on it. We have the number because a hook wrote every attempt down.&lt;/p&gt;

&lt;p&gt;Claude Code already hands you part of this. Its &lt;code&gt;InstructionsLoaded&lt;/code&gt; hook fires whenever a &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;.claude/rules/&lt;/code&gt; file enters context and reports why: &lt;code&gt;session_start&lt;/code&gt;, &lt;code&gt;nested_traversal&lt;/code&gt;, &lt;code&gt;path_glob_match&lt;/code&gt;, &lt;code&gt;include&lt;/code&gt; or &lt;code&gt;compact&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&gt;). Rulestack has a good write-up on logging it by itself (&lt;a href="https://dev.to/rulestack/did-claude-code-actually-load-your-rules-context-lazy-loading-and-the-instructionsloaded-hook-4dg0"&gt;Did Claude Code actually load your rules?&lt;/a&gt;). On the bus it's one more event going into the same log.&lt;/p&gt;

&lt;p&gt;Here's the whole first version of the bus, &lt;code&gt;.claude/bus/bus.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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Every hook event comes through here. Step one: write down what happened.&lt;/span&gt;
jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'{ts: (now | todate), event: .hook_event_name, tool: .tool_name,
        file: (.tool_input.file_path // .file_path), reason: .load_reason,
        kind: ([(.last_assistant_message // "") | scan("^\\[([a-z]+)\\]")[0]] | first)}
       | with_entries(select(.value != null))'&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="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;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/log.jsonl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable with &lt;code&gt;chmod +x .claude/bus/bus.sh&lt;/code&gt;, and make sure &lt;code&gt;jq&lt;/code&gt; is on your &lt;code&gt;PATH&lt;/code&gt;. From then on, every event becomes one line in &lt;code&gt;log.jsonl&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;{"ts":"2026-09-30T09:27:55Z","event":"InstructionsLoaded","file":"/repo/CLAUDE.md","reason":"session_start"}
{"ts":"2026-09-30T09:27:55Z","event":"UserPromptSubmit"}
{"ts":"2026-09-30T09:27:55Z","event":"InstructionsLoaded","file":"/repo/.claude/rules/docs.md","reason":"path_glob_match"}
{"ts":"2026-09-30T09:27:55Z","event":"PreToolUse","tool":"Write","file":"/repo/docs/cache.md"}
{"ts":"2026-09-30T09:27:55Z","event":"Stop","kind":"status"}
{"ts":"2026-09-30T09:27:55Z","event":"Stop"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And two questions you can already answer, how often each event fires and how many replies skipped their kind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ jq -s 'group_by(.event) | map({(.[0].event): length}) | add' .claude/bus/log.jsonl
{"InstructionsLoaded":2,"PreToolUse":1,"Stop":2,"UserPromptSubmit":1}
$ jq -s 'map(select(.event == "Stop"))
    | {replies: length, missing_kind: map(select(.kind == null)) | length}' \
    .claude/bus/log.jsonl
{"replies":2,"missing_kind":1}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the telemetry layer: which rule files loaded and why, which tools ran on which files, and which replies skipped their kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  With great responsibility comes great power
&lt;/h2&gt;

&lt;p&gt;Concentrating the events in one place and adding your own logic on top of the harness is a huge responsibility. The power it gives you is just as big.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scattered pieces come together.&lt;/strong&gt; Once your rules move onto it, they live in one folder, each with its moment in its own header, and one log records what happened. Listing every rule with its condition is a &lt;code&gt;grep&lt;/code&gt;. Seeing what fired last Tuesday is a &lt;code&gt;jq&lt;/code&gt; query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can bend it to what you need.&lt;/strong&gt; A &lt;code&gt;settings.json&lt;/code&gt; matcher can only filter on what Claude Code puts there: the event, plus one field it picks per event, such as the tool name. &lt;code&gt;InstructionsLoaded&lt;/code&gt;'s five reasons are all about files or the session. A script can match on anything it can read, and the bus sees every event, including what the last reply declared. Put a domain next to the kind, &lt;code&gt;[status][migration]&lt;/code&gt;, save it when the reply ends, and a rule can load for the work instead of for a file:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PreToolUse&lt;/span&gt;
&lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Write|Edit&lt;/span&gt;
&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="s"&gt;/docs/*&lt;/span&gt;
&lt;span class="na"&gt;declared&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migration&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
When you document a migration, name its down step and the release that stops reading the old column.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdbe6n9w7eaeivlzq4330.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdbe6n9w7eaeivlzq4330.png" alt="One rule file, migration-docs.md, with four annotations. on: PreToolUse is when, the hook event. tool: Write|Edit and path: */docs/* are where, the tool and the file. declared: migration is why, the work the last reply declared. The body is what, the text that gets delivered." width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;on&lt;/code&gt; is when, &lt;code&gt;tool&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt; are where, &lt;code&gt;declared&lt;/code&gt; is why, and the body is what gets delivered. This rule loads when the agent writes under &lt;code&gt;docs/&lt;/code&gt; while the work is a migration, and stays out of every other turn. The bus can only read the declaration once the reply is finished, so it applies from the next turn on. The domain sticks until another one is declared; the kind belongs to one reply. Getting there takes one more step in the bus: for each rule, check its &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;tool&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt; and &lt;code&gt;declared&lt;/code&gt; against the event and the saved declaration, and deliver the body of every rule that matches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You stop being locked to one agent.&lt;/strong&gt; The rules and the log are plain files in your repository. The only part of the bus that knows it's talking to Claude Code is the part that reads the hook's input. The other agents that ship hooks send the same kind of input. Codex reads hooks from &lt;code&gt;.codex/hooks.json&lt;/code&gt; with largely the same event names as Claude Code, and passes the finished reply as &lt;code&gt;last_assistant_message&lt;/code&gt;, the same field the bus reads (&lt;a href="https://learn.chatgpt.com/docs/hooks" rel="noopener noreferrer"&gt;Codex hooks&lt;/a&gt;). Cursor reads &lt;code&gt;.cursor/hooks.json&lt;/code&gt; with names of its own, such as &lt;code&gt;beforeSubmitPrompt&lt;/code&gt;, &lt;code&gt;preToolUse&lt;/code&gt; and &lt;code&gt;stop&lt;/code&gt; (&lt;a href="https://cursor.com/docs/hooks" rel="noopener noreferrer"&gt;Cursor hooks&lt;/a&gt;). Gemini CLI reads hooks from its &lt;code&gt;settings.json&lt;/code&gt;, with &lt;code&gt;BeforeTool&lt;/code&gt; and &lt;code&gt;AfterAgent&lt;/code&gt; (&lt;a href="https://geminicli.com/docs/hooks/reference/" rel="noopener noreferrer"&gt;Gemini CLI hooks&lt;/a&gt;). All of them pass a JSON payload on stdin with a &lt;code&gt;hook_event_name&lt;/code&gt; field, and all of them read exit code 2 as "block this". The answers they expect back differ, so one small adapter per agent maps its event names in and its answers out, and the same rules and the same log serve all of them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Hooks live in&lt;/th&gt;
&lt;th&gt;A few of its event names&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.claude/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UserPromptSubmit&lt;/code&gt;, &lt;code&gt;PreToolUse&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.codex/hooks.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UserPromptSubmit&lt;/code&gt;, &lt;code&gt;PreToolUse&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.cursor/hooks.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;beforeSubmitPrompt&lt;/code&gt;, &lt;code&gt;preToolUse&lt;/code&gt;, &lt;code&gt;stop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BeforeTool&lt;/code&gt;, &lt;code&gt;AfterAgent&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The responsibility part
&lt;/h2&gt;

&lt;p&gt;For the rules in &lt;code&gt;.claude/rules/&lt;/code&gt;, Claude Code decides when they load. For the rules on your bus, you do. Nothing tells you which moment a rule belongs to, which two rules now say opposite things on the same turn (&lt;a href="https://reporails.com/articles/opus-5-cost-instruction-conflicts" rel="noopener noreferrer"&gt;Opus 5: Cost of Instruction Conflicts&lt;/a&gt;), or which one hasn't loaded since you renamed a folder. Rename &lt;code&gt;docs/&lt;/code&gt; to &lt;code&gt;doc/&lt;/code&gt; and the docs rule never matches again. Start declaring &lt;code&gt;[db]&lt;/code&gt; where you used to declare &lt;code&gt;[migration]&lt;/code&gt;, and the migration rules go quiet the same way. Either silence looks exactly like a week where nobody touched the docs or a migration.&lt;/p&gt;

&lt;p&gt;The log shows what loaded. It can't show a rule that should have loaded and didn't, or whether a rule that did was worded well enough to steer anything. How you keep a rule set organized once the harness stops doing it for you is the next piece.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt;, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface you wrote down and tells you, with measured evidence, which instructions couple to behavior and which are text the model can ignore.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Progressive Disclosure: Shaping Claude Code's Output</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Wed, 23 Sep 2026 13:26:12 +0000</pubDate>
      <link>https://dev.to/reporails/progressive-disclosure-shaping-claude-codes-output-4dg4</link>
      <guid>https://dev.to/reporails/progressive-disclosure-shaping-claude-codes-output-4dg4</guid>
      <description>&lt;p&gt;Opus 5.5 shipped on September 22, and its replies read differently: answer first, less throat-clearing. Box, testing it before launch, found its answers "40% less verbose without losing accuracy", and Factory measured "20 to 25% fewer output tokens" (&lt;a href="https://www.anthropic.com/claude-opus-5-5" rel="noopener noreferrer"&gt;Introducing Claude Opus 5.5&lt;/a&gt;). Those are two teams' numbers on their own workloads, and they point the same way.&lt;/p&gt;

&lt;p&gt;The shape of the reply is still yours to set. A shorter default helps the rename and shortchanges the design question, and what you actually want is both: two lines when you ask for a rename, the full walkthrough when you ask whether the cache belongs in Redis, on every turn, without saying which each time. That makes it a loading problem. The instruction that shapes the reply has to be in front of the model on the turn it applies and out of the way on the others.&lt;/p&gt;

&lt;p&gt;The first piece in this series split progressive disclosure into three handles: what loads, where it loads, and when (&lt;a href="https://reporails.com/articles/progressive-disclosure-what-where-when-and-why" rel="noopener noreferrer"&gt;Progressive Disclosure: What, Where, When, and Why&lt;/a&gt;). This one takes a single concern, the shape of the reply, and walks it up through every place Claude Code will load an instruction from. Each step gets further than the last and ends on its own limit. Then we go one level up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output style: one shape for the whole session
&lt;/h2&gt;

&lt;p&gt;Claude Code has a setting for exactly this. An output style is "a set of instructions that sets Claude's role, tone, and response format for every response in a session", and the built-in &lt;code&gt;Concise&lt;/code&gt; style is pure reply shape: the first sentence states the answer, the lead-in, the step-by-step narration and the closing recap go, a simple question gets one to three sentences, and the engineering work stays as thorough as in the default style (&lt;a href="https://code.claude.com/docs/en/output-styles" rel="noopener noreferrer"&gt;output styles&lt;/a&gt;). Pick it with &lt;code&gt;/output-style concise&lt;/code&gt;, or write your own as a markdown file in &lt;code&gt;.claude/output-styles/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reason to reach for it is where it sits. The style's instructions travel with the system prompt on every request. Your &lt;code&gt;CLAUDE.md&lt;/code&gt; "is delivered as a user message after the system prompt, not as part of the system prompt itself" (&lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;memory&lt;/a&gt;), and Anthropic's prompting guide, for this exact problem, says to "use direct instructions in the system prompt: 'Respond directly without preamble'" (&lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices" rel="noopener noreferrer"&gt;prompting best practices&lt;/a&gt;). The rule reads the same in both places; the output style gives it the higher seat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqky73p6ewkgqhxnlrs1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqky73p6ewkgqhxnlrs1.png" alt="The request the model reads: tools, then the system prompt, then messages. The output-style rule is lit inside the system prompt. The same rule sits dimmed in the CLAUDE.md user message after it, next to the MEMORY.md index line loaded at session start." width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the lean root from the first piece: a rule that applies to every reply earns its always-on slot. Its limit is built into the definition, though. It's one shape for the whole session, so the rename and the design question get the same one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path rules: triggered by the file
&lt;/h2&gt;

&lt;p&gt;Rules that apply only sometimes go in &lt;code&gt;.claude/rules/&lt;/code&gt;, one topic per file. Give a rule a &lt;code&gt;paths&lt;/code&gt; field and it becomes conditional: path-scoped rules "trigger when Claude reads files matching the pattern", and stay out of context otherwise (&lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;memory&lt;/a&gt;).&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs/**"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
When answering about documentation, open with the context the reader needs, then the change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That rule shows up when Claude reads a file under &lt;code&gt;docs/&lt;/code&gt; and stays out of the rename in &lt;code&gt;src/app/nav.tsx&lt;/code&gt;. What triggers it is the file the agent touches. A design question can arrive while the agent has any file open, and no glob tells a design question from a rename.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff3xk0aiog2wvgxoi9x1p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff3xk0aiog2wvgxoi9x1p.png" alt="A path-scoped rule whose paths field matches a directory. On a turn reading a file outside it the rule stays out of context; on a turn reading a matching file it loads." width="799" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto memory: the model decides what loads
&lt;/h2&gt;

&lt;p&gt;Auto memory is the built-in handle that comes closest to loading on relevance. Claude saves "corrections you give Claude and approaches you confirm" into one directory per project, &lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/memory/&lt;/code&gt;. The first 200 lines (or 25KB) of the &lt;code&gt;MEMORY.md&lt;/code&gt; index load at the start of every session. The topic files behind it don't: Claude "reads them on demand using its standard file tools when it needs the information" (&lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;memory&lt;/a&gt;). Correct the shape of a design answer a couple of times and you may find a one-line entry, &lt;code&gt;design answers: trade-offs first&lt;/code&gt;, with the full note behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2aqsp9qqpndrqkulvi7s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2aqsp9qqpndrqkulvi7s.png" alt="The MEMORY.md index is loaded every session, one entry reading design answers: trade-offs first. On a turn asking to rename a function, the topic file behind that line stays unread. On a turn asking whether to cache in Redis or in memory, Claude reads it and the full reply-shape note enters context." width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The index is always there and the detail comes in when it's needed. Which detail, and when, is Claude's call, made from a line Claude wrote. You can open and edit every one of these files with &lt;code&gt;/memory&lt;/code&gt;, but by default Claude does the writing, and the docs call auto memory "machine-local": the laptop that learned how you like design answers is the only machine that knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  UserPromptSubmit: you decide, inside a regex
&lt;/h2&gt;

&lt;p&gt;To take the routing back, Claude Code runs a &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook on every message before Claude sees it. The hook gets your text in the &lt;code&gt;prompt&lt;/code&gt; field on stdin, and plain stdout on a clean exit is added as context Claude can see (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&gt;). So you classify the turn yourself, in &lt;code&gt;.claude/hooks/reply-shape.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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# UserPromptSubmit hook: add the reply shape that fits this kind of turn.&lt;/span&gt;
&lt;span class="nv"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.prompt // ""'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qiE&lt;/span&gt; &lt;span class="s1"&gt;'\b(why|should we|trade-?offs?|design|architecture|compare)\b'&lt;/span&gt; &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;$prompt&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;"Reply shape for this turn: a design question. Lay out the options and their trade-offs, then recommend one."&lt;/span&gt;
&lt;span class="k"&gt;elif &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qiE&lt;/span&gt; &lt;span class="s1"&gt;'\b(rename|typo|bump|move|delete)\b'&lt;/span&gt; &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;$prompt&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;"Reply shape for this turn: a routine edit. Make the change, then answer in one or two sentences."&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it under &lt;code&gt;hooks.UserPromptSubmit&lt;/code&gt; in &lt;code&gt;.claude/settings.json&lt;/code&gt;. The event takes no matcher, so it runs on every prompt:&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;"hooks"&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;"UserPromptSubmit"&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;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&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_PROJECT_DIR/.claude/hooks/reply-shape.sh"&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="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;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqq3ngbk37j8u4vipeu93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqq3ngbk37j8u4vipeu93.png" alt="The flow of a UserPromptSubmit hook: your message goes to reply-shape.sh, which reads the prompt field. A design question adds the line lay out the trade-offs, then recommend. A routine edit adds the line one or two sentences. Anything else adds nothing. Claude then answers with that line in context." width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the decision is yours, made the same way on every turn. Look at what happened to the rule on the way. "Lay out the trade-offs, then recommend" is a string inside an &lt;code&gt;echo&lt;/code&gt;, its condition is a regex two lines above it, and whether any of it runs is decided in a JSON file somewhere else. "Rename the design tokens" matches both lists, and the first match wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop: a check after the fact
&lt;/h2&gt;

&lt;p&gt;The last handle runs once the reply is written. A &lt;code&gt;Stop&lt;/code&gt; hook receives the finished reply as &lt;code&gt;last_assistant_message&lt;/code&gt;; exit with code 2 and the stop is blocked, with your stderr fed back to Claude to try again. &lt;code&gt;stop_hook_active&lt;/code&gt; is true when a Stop hook already sent the reply back, which is your loop guard (&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&gt;). Save this one as &lt;code&gt;.claude/hooks/gate-length.sh&lt;/code&gt; and wire it under &lt;code&gt;hooks.Stop&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Stop hook: send an over-long reply back once.&lt;/span&gt;
&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&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="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.stop_hook_active // false'&lt;/span&gt; &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;$input&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="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&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;exit &lt;/span&gt;0

&lt;span class="nv"&gt;words&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.last_assistant_message // ""'&lt;/span&gt; &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;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="si"&gt;)&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;$words&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 180 &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;"Your reply ran &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;words&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; words; the budget is 180. Put the result first and cut the recap."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fla54xrb253djfq7jxbi4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fla54xrb253djfq7jxbi4.png" alt="The Stop hook loop: the model finishes a turn, the hook reads the reply from last_assistant_message, then checks it against the 180-word budget. Over budget, it exits 2 and sends the reply back once; within budget, it exits 0 and the turn ends. The stop_hook_active guard blocks once, then stands down." width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one is a check. It loads nothing into the model's context before the reply; it measures what came out and sends it back. It also shares the output style's blind spot: 180 words is the budget for the rename and the design question alike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the harness comes short
&lt;/h2&gt;

&lt;p&gt;This claim is easy to overstate, so here it is precisely. The harness does load on relevance: memory topic files and skills come in when they're needed. Rules can declare paths. A hook can route by the kind of turn, as &lt;code&gt;reply-shape.sh&lt;/code&gt; does. Each handle did its job. What the harness does is deliver rules. Organizing them is left to you, and that shows up in four places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A rule's text and its loading condition live apart.&lt;/strong&gt; The design-answer rule is an &lt;code&gt;echo&lt;/code&gt; string, its condition is a regex in the same script, and its switch is an entry in &lt;code&gt;settings.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loading conditions come in five forms.&lt;/strong&gt; Always on, a path glob, the model's judgment, a regex on the prompt, a check on the finished reply. Each form follows what the harness exposes at that point (a file read, the prompt text, the reply), and none follows the situation you care about, which is what kind of turn this is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing lists every rule with its condition, and nothing tests whether two collide.&lt;/strong&gt; The output style says result first; the docs rule says context first; on a design question about the docs, both are in the window. The model picks one and raises no error (&lt;a href="https://reporails.com/articles/opus-5-cost-instruction-conflicts" rel="noopener noreferrer"&gt;Opus 5: Cost of Instruction Conflicts&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing lets the reply's shape change with the kind of turn and bring its own checks along.&lt;/strong&gt; The Stop hook's budget has no idea the turn was a design question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk70q5hdbcd2cvqzw5cz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk70q5hdbcd2cvqzw5cz6.png" alt="Six cards for one concern: the output style, a docs rule, CLAUDE.md, two hook scripts in the repository, and the Claude-written MEMORY.md on one machine. Amber links mark two pairs that cannot both hold: result first against context first, and one or two sentences against the full walkthrough." width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One level up: the reply declares its kind
&lt;/h2&gt;

&lt;p&gt;So we moved the problem up a level in our own setup. The agents that build Reporails open every reply with a declared kind: &lt;code&gt;[status]&lt;/code&gt; for a work report, &lt;code&gt;[analysis]&lt;/code&gt; for reasoning, &lt;code&gt;[narration]&lt;/code&gt; for how something works, &lt;code&gt;[handover]&lt;/code&gt; for the brief a session hands its successor, plus domain tokens such as &lt;code&gt;[surface]&lt;/code&gt; when the work is the public site. That one token decides two things: which reply-shape rules apply, and which checks run when the reply ends.&lt;/p&gt;

&lt;p&gt;In practice, &lt;code&gt;[narration]&lt;/code&gt; relaxes the check that counts separate blocks in a reply, since an explanation reads as paragraphs, and keeps the 150-word length check. &lt;code&gt;[handover]&lt;/code&gt; relaxes seven checks that a recap of finished work would trip by construction, and the check that stops a reply from handing new work back to me still runs. &lt;code&gt;[surface]&lt;/code&gt; switches on a periodic re-read of our visual-review rules, but only while that domain is the current work, so a research session never pays for it. A mislabelled reply still hits every check its label doesn't relax, so the token scopes the checks without becoming a way around them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm89fbh5yd7fsjgjxnia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm89fbh5yd7fsjgjxnia.png" alt="A reply opens with a declared kind. The kind selects its row in a class map kept outside the harness. Narration: block-count check relaxed, 150-word length check on. Handover: seven report checks relaxed, the no-work-pushed-to-the-operator check on. Surface: a periodic re-read of the visual-review rules, on only while the domain is current. One Stop hook delivers the selected rules and checks." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's output style done as progressive disclosure. The shape rules load according to the kind of turn, the rules and the map from kind to rules live outside the harness as plain files, and the harness only delivers them, through one Stop hook. Each of those files states the situation it applies to next to its own text, which also covers rules that no harness handle can express directly. One of ours reads, in effect, "load this before the agent writes a file in our knowledge base". Path rules fire when Claude reads a matching file, and the moment we need comes before it writes one. You could build that with a &lt;code&gt;PreToolUse&lt;/code&gt; hook, with the glob in yet another script. In our setup the condition sits in the rule's own header.&lt;/p&gt;

&lt;p&gt;One Stop hook is the naive version of this. Look at what it needs underneath. Each rule names the moment it applies to: before a write to the knowledge base, at the end of a reply of a given kind, every few turns while a domain is the current work. The harness fires its own events (a prompt arrives, a tool is about to run, a reply ends), and none of them is one of those moments. Something has to sit between the two, turning each harness event into the moment it represents and handing over only the rules that asked for that moment. That something is an event bus, and why a rule set outgrows its harness without one is the next piece.&lt;/p&gt;

&lt;p&gt;Once the rules live one level up, the question you ask changes from where a rule goes to what the whole set does on this turn.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt;, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface you wrote down and tells you, with measured evidence, which instructions couple to behavior and which are text the model will ignore.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>tutorial</category>
      <category>agents</category>
    </item>
    <item>
      <title>Progressive Disclosure: What, Where, When, and Why</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Wed, 16 Sep 2026 18:01:55 +0000</pubDate>
      <link>https://dev.to/reporails/progressive-disclosure-what-where-when-and-why-36m3</link>
      <guid>https://dev.to/reporails/progressive-disclosure-what-where-when-and-why-36m3</guid>
      <description>&lt;p&gt;Do you remember when we first started using AGENTS.md files? &lt;/p&gt;

&lt;p&gt;You would have a project root file describing the project, and some nested ones describing the content in the folder where they reside. This opened up exciting possibilities for everyday users: For the first time you could put a rule where the work was and trust it to show up only there. I ran an entire project this way once, one small instruction file sitting in each layer of the codebase. &lt;/p&gt;

&lt;p&gt;You only had to keep the mental model locked, and you could manage your instructions so they loaded only when the agent was working in the folders they belonged to.&lt;/p&gt;

&lt;p&gt;This was one of the earliest forms of progressive disclosure, and coding agents still support it to this day. It comes with a price, though. Relevant instructions only load if the agent is working in your folder. So you hit the classic context rot problem right away, because you might be working on the backend while some frontend pieces need adjusting along with it. Having the coding agent work front and backend at once always causes mayhem: the instructions compete for attention, and the system has to split its attention between the two big areas.&lt;/p&gt;

&lt;p&gt;Cursor and Claude were quick to offer a finer-grained solution here. Instead of scattering some number of AGENTS/CLAUDE.md files through the subfolders, you use a harness artifact that carries the path configuration for where to load. Not just folder level, but file level. Dandy. Problem solved? Not even close.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;So what is the problem itself? To see why even file-level scoping is not the finish line, go back to the crudest option for a second. Wouldn't it be easier to just load everything into the context at one go and leave the LLM to figure out the rest?&lt;/p&gt;

&lt;p&gt;That is what most of us tried first, and it is the reason your CLAUDE.md is three hundred lines long, the kind of file people now argue you should just delete (&lt;a href="https://reporails.com/articles/opus-5-delete-your-claudemd" rel="noopener noreferrer"&gt;Opus 5: Delete your CLAUDE.md?&lt;/a&gt;). Every rule you ever wrote, in the window every turn, always in reach. It feels like the careful choice, but it is the expensive one, and tokens are the smallest part of the bill.&lt;/p&gt;

&lt;p&gt;A model does not work through your instructions the way you work through a checklist, line by line, giving each one its moment. It spreads a fixed amount of attention across everything in front of it, and every line you add is one more thing pulling on that same budget. Ten rules and each gets a real share. A hundred, and the one rule that matters for this turn is a single voice in a crowded room, shouting over ninety-nine others that have nothing to do with the task at hand. It is the front-and-backend problem again, the one from a moment ago, except now you have signed the agent up for it permanently: every turn, every rule, all at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famyxhrvshbce89nrg5by.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famyxhrvshbce89nrg5by.png" alt="Two context windows side by side. On the left, load everything: a hundred faint rule bars crowd the window and the one rule that matters this turn is lost mid-crowd, marked 100 in context. On the right, load what the turn needs: the window holds three rules, the one for this turn glowing brightest, marked 3 in context. The room empties, one of three instead of one of a hundred." width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not just a hunch about attention. When we went through 28,721 real repositories, the median instruction file carried about fifty items and only a dozen of them were actual directives. The rest was scaffolding the model still reads past on every turn: most of your always-on file is not even telling the agent to do anything, and all of it competes for the same budget. We wrote that up as &lt;a href="https://reporails.com/articles/state-ai-instruction-quality-30k-repo-analysis" rel="noopener noreferrer"&gt;The State of AI Instruction Quality&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So "load everything" does not dodge the problem. It is the problem, turned all the way up.&lt;/p&gt;

&lt;p&gt;That is what the path configuration was for. Loading the payments rule only when the agent is actually in the payments code is the honest fix: keep each rule out of the crowd until the turn it belongs to. Real progress, and still only part of the answer, because where a rule lives is one lever and there are others.&lt;/p&gt;

&lt;p&gt;A rule can be the wrong thing to load, not only loaded in the wrong place. It can be something you want at a moment rather than in a folder. What you actually want is a way to decide, for every rule you have, what loads, where, and when. That already has a name.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: Progressive Disclosure
&lt;/h2&gt;

&lt;p&gt;What is progressive disclosure? It is the procedure for how you gradually introduce the relevant instructions and context to the coding agent. Instead of one always-on file, you hand the agent each rule at the moment it is about to matter, and keep it out of the way the rest of the time. That is the whole idea. What makes it something you can build with is that it comes apart into three handles, and once you can see them separately you stop reaching for the root file by reflex.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqk1je89q6f427zm6vp44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqk1je89q6f427zm6vp44.png" alt="Three dark panels labeled what loads, where it loads, and when it loads, each showing an example rule (skills/commit-style, rules/api/**, on: session-start) that lights up on its cue and stays dark otherwise." width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is also the fix Anthropic now writes down in as many words. In its guidance for the Claude 5 generation, Anthropic's Thariq Shihipar names the habit that grew your file, the "myth ... that you want to make these a central repository for every known practice," and the cure, to "use progressive disclosure heavily" and let the rest load from skills the file points at (&lt;a href="https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models" rel="noopener noreferrer"&gt;the new rules of context engineering&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  What loads
&lt;/h3&gt;

&lt;p&gt;A rule does not have to be a line in a file the agent reads top to bottom. Package it as a skill: a self-contained instruction the agent pulls in when the task calls for it and never sees otherwise. Your commit conventions have no business in the window while the agent is chasing a layout bug, and a skill keeps them out of it until you reach for &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it loads
&lt;/h3&gt;

&lt;p&gt;You have already met this one. A rule that only governs the payments code lives next to the payments code: folder level with a nested file, or file level with the path configuration Cursor and Claude added. It shows up on the turns the agent works there and is not in the room the rest of the time. The rule did not get weaker; it got aimed.&lt;/p&gt;

&lt;h3&gt;
  
  
  When it loads
&lt;/h3&gt;

&lt;p&gt;Some rules only matter at a moment, not in a place. Tie them to the moment: pull a rule in when a session starts, or when a certain kind of work begins, and keep it dark until then. Same rule, later entrance. You are not rewriting it; you are deciding when it walks on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why
&lt;/h3&gt;

&lt;p&gt;None of the three changes what a rule says. They change when the agent has to carry it. And what you are short on is not disk space or even tokens; it is the agent's attention on the turn that counts. What, where, and when are three ways to spend that attention on the rule that matters instead of the ninety-nine that do not. Get them right and the three-hundred-line always-on file comes apart into a lean root plus a set of rules that show up on cue: the crowded room empties, and the rule for this turn is one of three in the window instead of one of a hundred. That is the loading problem solved, or as solved as loading gets, the right rule in front of the agent, uncrowded, exactly on the turn it applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The file was never the unit
&lt;/h2&gt;

&lt;p&gt;Something changed while you were splitting that file, and it is bigger than the file. You did not tidy a document. You built a system: a lean root, and a set of rules that each load on cue, some pinned to a path, some to a moment. And a system has a shape you can no longer take in by scrolling one file. Is this rule still loading where you think it is? Did you scope that one so tightly it now reaches nobody? Are two of them set to load on the same turn, back to competing for attention, the crowded room rebuilt in miniature? Or worse, are two of them quietly telling the agent opposite things, the kind of contradiction we put a price on in &lt;a href="https://reporails.com/articles/opus-5-cost-instruction-conflicts" rel="noopener noreferrer"&gt;Opus 5: Cost of Instruction Conflicts&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhjq9790dmvgbp6w9xyd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhjq9790dmvgbp6w9xyd.png" alt="A single tall always-on 300-line AGENTS.md file on the left comes apart into a lean root file plus five small rule cards on the right, each scoped to a path or a moment and loading on cue, forming a small connected system." width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Is it in the file" was the right question when there was one file. There is no one file now. The question is what actually loads, where, and when, and whether that still matches what you meant. None of it lives in your root file anymore, because you moved it out yourself.&lt;/p&gt;

&lt;p&gt;That is the ground the rest of this series stands on. Each piece takes one handle and goes deep, with the code to run it: what loads, and how you would know it did; where to scope a rule so it stops diluting every unrelated turn; when to move a rule off the page and onto an event. (And a rule can load perfectly and the agent can still ignore it. That is a different problem with a different fix, and it earns its own piece.)&lt;/p&gt;

&lt;p&gt;When you want to see the shape of what you have built, which rules still load and where two of them now collide, that is the read &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt; gives you: free, on your own machine, on the files you wrote. It reads what you author; it never touches your agent while it runs.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>performance</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Opus 5: How to Review Generated Code</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Thu, 27 Aug 2026 15:56:58 +0000</pubDate>
      <link>https://dev.to/reporails/opus-5-how-to-review-generated-code-4g8l</link>
      <guid>https://dev.to/reporails/opus-5-how-to-review-generated-code-4g8l</guid>
      <description>&lt;p&gt;So, just another Tuesday. You ask Opus 5 for a one-line fix: a date parser is choking on a timezone suffix, change the format string. Twenty seconds later the agent reports done. You open the diff and it is not one line. It is eleven files. It rewrote the parser, renamed a helper three modules over, tidied an import block you never mentioned, and added a caching layer nobody asked for. Every test is green (actually have 11 more). The check passed, everyone's happy, except you, who now have to decide whether to read four hundred lines to approve a change that was supposed to be one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reporails.com/articles/opus-5-review-bottleneck" rel="noopener noreferrer"&gt;The first half of this pair&lt;/a&gt; named why that decision is the hard part now. Opus 5 made writing nearly free and moved the whole cost onto review, and the reflex fix, pointing a second model at the first one, rebuilds the problem one guess at a time. That piece stopped at the diagnosis. This one is the other half: what you put in place of the second model, so the eleven-file surprise gets caught by something other than your attention at 6pm.&lt;/p&gt;

&lt;p&gt;Anthropic documents that scope blowout as expected behavior. Its own &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5" rel="noopener noreferrer"&gt;Opus 5 guidance&lt;/a&gt; names it plainly: the model "can also expand the scope of a task, adding steps that weren't requested or applying its own judgment about what the task should be." A more capable model that runs longer and finishes more of the job also finishes more of the job you did not ask for. Catching that reliably takes three controls, and they are the ordinary layers of defense in depth: steer the model off the mistake, review what it did, and put a deterministic gate under both. Two of the three keep the mistake out and differ only in how far you can trust them, steering by moving the odds and the gate by refusing outright; review is how you catch what slips between them. What follows names each layer for an agent, ordered by cost rather than by when each runs, but the layered shape itself is the standard one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06w4j3af3lh5260go4qr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06w4j3af3lh5260go4qr.png" alt="Three layers stand between an agent's eleven-file change and the main branch, left to right: a steering layer that constrains the task to a named set of paths, a review layer where a person runs the change and probes its tests, and an enforcement layer that refuses any write outside the scoped set. Two of the layers keep the change in scope, steering and the gate, and the middle one catches what slips past." width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer one: steering, and the instruction to delete
&lt;/h2&gt;

&lt;p&gt;The instinct, when the agent overreaches, is to write a rule telling it to check itself. Add &lt;code&gt;verify your work before finishing&lt;/code&gt; to your &lt;code&gt;CLAUDE.md&lt;/code&gt; and move on. On Opus 5 that line costs you and buys nothing. The same &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5" rel="noopener noreferrer"&gt;Opus 5 guidance&lt;/a&gt; is blunt about it: "Claude Opus 5 verifies its own work without being told to. If your prompt contains explicit verification instructions ... remove them," because "instructions like these cause over-verification," and removing them "reduces wasted tokens with no loss in quality." The model already double-checks. Your instruction just makes it burn tokens double-checking louder, and none of that self-checking sees the problem you actually have. A model verifying its own work confirms the eleven files do what the model intended. It has no way to know the model was asked for one.&lt;/p&gt;

&lt;p&gt;What belongs in the steering layer is the scope constraint, not the verification order. Anthropic gives you a starting shape for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Deliver what was asked, at the scope intended. Make routine judgment
calls yourself, and check in only when different readings of the request
would lead to materially different work. If the request seems mistaken or
a better approach exists, say so in a sentence and continue with the task
as asked rather than quietly narrowing, widening, or transforming it.
Finish the whole task, and stop short of actions that are clearly beyond
what was asked.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That block points the model the right way, and its last line already binds: it names three specific failure modes (narrowing, widening, transforming) and says what to do instead. Two of its other lines lean on words the model has to interpret, and a rule the model interprets is a rule it can set aside. "At the scope intended" asks the model to measure its edits against an intention nobody wrote down as anything checkable. "Check in only when different readings would lead to materially different work" leaves &lt;em&gt;material&lt;/em&gt; for the model to define, so the threshold floats. "Stop short of actions clearly beyond what was asked" makes &lt;em&gt;clearly&lt;/em&gt; the reader's judgment rather than a line the model can hold an action against.&lt;/p&gt;

&lt;p&gt;Give the model a target it can check every edit against instead, starting with the one thing only you can supply: the set of paths this task is allowed to touch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Write this task's scope as a named set of paths before you change
anything. For this date-parser fix the set is &lt;span class="sb"&gt;`src/dates/`&lt;/span&gt; and its
tests; write your own and keep it in view.

Change only files in the named set. Touch a path outside it only when
the change will not run otherwise, and say so before you edit it.

Make the smallest change that satisfies the request: no caching layer,
no helper rename, no refactoring pass, no new abstraction, even when it
would improve the code.

Check in before starting only when the request could mean different
files, and finish every file in the set the change needs.

If a larger change would help, name it in one sentence and make only the
asked-for one.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line now points back at one concrete anchor, the named set, and a rule pinned to a name you wrote down keeps its grip as the context around it grows; an abstract rule fades into it. That is the filter: a line that cannot cite the named set does not earn its place, which is what dropped the vague check-in and the maybe-later hedge from the version above. The block ships pointing at that set; you make it bind by writing your real paths into the top. &lt;/p&gt;

&lt;p&gt;That same named set is what Layer three turns into a hard gate, so steering and enforcement guard the same line: one asks the model to stay inside it, the other refuses the write when it strays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Farbne21qm55pqijhh51h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Farbne21qm55pqijhh51h.png" alt="Two rule blocks side by side. On the left, a steering block whose words the model must interpret, with at the scope intended, materially, and clearly highlighted as phrases the model can weigh and set aside. On the right, a block built around a named set of paths, src dates, that the model can check every edit against." width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That &lt;em&gt;is&lt;/em&gt; steering: it moves the odds that the next diff stays inside the ask. It does not set them to one, because everything on the context surface is an input to a probabilistic process, an instruction the model is free to weigh and set aside on the exact turn it decides the caching layer is a good idea. Steering is the first layer because it is the cheapest and it handles the common case. Lean on it for the routine change, and reach past it for the one you cannot afford to get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer two: procedure, and reviewing what actually ran
&lt;/h2&gt;

&lt;p&gt;When a human does open the diff, the skim is the failure mode &lt;a href="https://reporails.com/articles/opus-5-review-bottleneck" rel="noopener noreferrer"&gt;the previous article&lt;/a&gt; identified as well: the LGTM speedrun, a 300-line diff approved in under three minutes on a gut feeling. The reflex answer is "read more carefully," and it runs out the moment the diff is larger than what fits in your head, which is exactly the diff a scope-expanding agent produces.&lt;/p&gt;

&lt;p&gt;A review that scales is a sorting job. Some of what a reviewer checks has a definite answer a rule can settle: whether the suite passes, whether the types hold, whether the change touched a path it was never scoped to, whether a secret landed in the diff. The rest needs a model of the world the change lives in, and only a person has that. The process is to send the first pile to something that runs every time without you, which is Layer three, and spend your own reading on the second.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjb4hyd9bbje2eng1d7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjb4hyd9bbje2eng1d7n.png" alt="A diff to review splits into two piles. One pile holds questions with a definite answer, does the suite pass, do the types hold, did it touch an out-of-scope path, did a secret land, routed to the machine. The other holds questions that need a model of the world, was the caching layer worth adding, does the rename help or hurt, was it the right fix, routed to you." width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is left once the machine-checkable part is routed away is a real review, and two practices carry most of it. Neither trusts the diff on sight.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Review by executing.&lt;/strong&gt; Run the code the agent wrote against the case you cared about in the first place. A diff can read as plausible and still behave differently once it runs, so the caching layer it added stays a claim until you exercise it. An executing reviewer, a person or a CI job that runs the change, tests what it actually does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probe the tests with a mutation.&lt;/strong&gt; A green suite tells you the tests pass. It does not tell you the tests would fail if the code were wrong. Change one thing the code should care about, revert a guard, flip a comparison, delete a branch, and confirm a test goes red. If nothing does, your tests pass no matter what the code does, and a scope-expanding agent leaves exactly that behind: new code, new tests, all green, and not one test that would notice if the new code were broken. This is mutation testing done by hand; tools like &lt;a href="https://github.com/boxed/mutmut" rel="noopener noreferrer"&gt;&lt;code&gt;mutmut&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/sixty-north/cosmic-ray" rel="noopener noreferrer"&gt;&lt;code&gt;cosmic-ray&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://stryker-mutator.io/" rel="noopener noreferrer"&gt;&lt;code&gt;Stryker&lt;/code&gt;&lt;/a&gt; automate the same probe, and practitioners reach for it precisely when an agent wrote both the code and the tests that vouch for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F98f2lzljoczeii51i5xs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F98f2lzljoczeii51i5xs.png" alt="Two terminals. On the left, one comparison in the code is flipped by hand so the code is now wrong on purpose. On the right, the test suite is run and still reports eight passed, zero failed. No test went red, so nothing in the suite would notice the code being wrong." width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A model can take the first pass over what remains, in one specific role. Anthropic's note on Opus 5 as a reviewer is worth reading against the "a model checking a model" warning from the first half: the model "reviews code with high precision and recall," and the guidance is to "ask it to report everything and filter in a separate pass." Use it as a fast first reader that surfaces candidates cheaply, with one rule: the reviewer cannot be the author. A model grading its own diff carries the blind spot that produced it, the same reason you do not review your own code by rereading it. A fresh instance of the same model clears the context it sat too close to, though not the blind spots baked into the model itself, so the strongest version of this pass is a different model or a person. Point a fresh reader at the change and filter its findings yourself, because deciding which of them matter, and whether the scope was even right, is judgment a second model cannot hand you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer three: enforcement, the gate that just refuses
&lt;/h2&gt;

&lt;p&gt;The instinct is to put the reviewer last, as the final gate before main. That instinct is the bug. A reviewer, a person skimming at 6pm or a model grading a model, is exactly what you cannot trust as the last word, so the deterministic gate goes under the review, not the reviewer over the gate.&lt;/p&gt;

&lt;p&gt;Steering moves the odds; the review reads what steering let through. The third layer runs every machine-checkable question automatically, so no person re-runs it. A rule that runs this way forms no opinion about the diff, so it returns the same verdict on the hundredth change as on the first, however large the change grows.&lt;/p&gt;

&lt;p&gt;Most of this layer is enforcement you may already own and may be running as advice instead of as a floor. The test suite, the type checker, the linter, a secret scanner: wire them as required checks in CI and put branch protection in front of the merge, so a failing check refuses the merge rather than leaving a red mark a tired reviewer waves through. Then add the checks the agent's own failure modes call for, a scope guard and a mutation gate among them. That is the outside enforcement, and it runs after the change is written, at the pull request.&lt;/p&gt;

&lt;p&gt;One piece of this layer CI cannot give you, because CI only sees the change after it is written: a gate that fires inside the agent loop, before the write lands. That is the one that fits the scope blowout.&lt;/p&gt;

&lt;p&gt;The gate for an agent that touches files it was never asked to touch is a blast-radius guard: refuse a write to any path outside the set this task was scoped to. Claude Code fires a &lt;code&gt;PreToolUse&lt;/code&gt; hook before a tool call executes, and an exit code of 2 blocks the call and hands your message back to the model (per the &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks docs&lt;/a&gt;). Save this as &lt;code&gt;.claude/hooks/guard-blast-radius.sh&lt;/code&gt; and make it executable:&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;# guard-blast-radius.sh: refuse a Write/Edit/MultiEdit outside the files this&lt;/span&gt;
&lt;span class="c"&gt;# task was scoped to. Runs as a Claude Code PreToolUse hook. Exit 2 blocks it.&lt;/span&gt;
&lt;span class="c"&gt;# Requires jq.&lt;/span&gt;
&lt;span class="nv"&gt;payload&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="c"&gt;# the pending tool call arrives as JSON on stdin&lt;/span&gt;
&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.file_path // ""'&lt;/span&gt; &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;$payload&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.cwd // ""'&lt;/span&gt; &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;$payload&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Claude Code passes file_path relative to cwd, but strip cwd anyway so the&lt;/span&gt;
&lt;span class="c"&gt;# allow-list holds whether the path arrives relative or absolute.&lt;/span&gt;
&lt;span class="nv"&gt;rel&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;path&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cwd&lt;/span&gt;&lt;span class="s2"&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="c"&gt;# Reject .. traversal, or src/dates/../../etc/passwd slips through the prefix.&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rel&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;..&lt;span class="k"&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;"blocked: '&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;' uses .. path traversal."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2&lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;

&lt;span class="c"&gt;# The blast radius for this task: the only paths a write is allowed to touch.&lt;/span&gt;
&lt;span class="c"&gt;# Keep it somewhere you edit per task, or read it from a file.&lt;/span&gt;
&lt;span class="nv"&gt;allowed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'^(src/dates/|tests/dates/)'&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;$path&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;exit &lt;/span&gt;0                      &lt;span class="c"&gt;# no path in this call, nothing to guard&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&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;"&lt;/span&gt;&lt;span class="nv"&gt;$allowed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &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;$rel&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;"blocked: '&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;' is outside this task's blast radius (&lt;/span&gt;&lt;span class="nv"&gt;$allowed&lt;/span&gt;&lt;span class="s2"&gt;). &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  If the change genuinely needs this file, widen the scope on purpose, not in passing."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;2   &lt;span class="c"&gt;# exit 2 == hard block; the stderr message goes back to the model&lt;/span&gt;
  &lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it to the write tools in &lt;code&gt;.claude/settings.json&lt;/code&gt;:&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;"hooks"&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;"PreToolUse"&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;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write|Edit|MultiEdit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&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/hooks/guard-blast-radius.sh"&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="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;Now the agent that was asked to fix a date parser tries to rename a helper three modules over. The transition fires before the write, the path fails the allow-list, and the write never lands. The model sees the refusal in the same turn and has to either stay in scope or say out loud that it needs to widen it, which is the conversation you wanted to have before eleven files changed, not after. The gate ignores the diff's contents and its size alike. It checks one path rule, the same way on the thousandth call as on the first.&lt;/p&gt;

&lt;p&gt;One line in that script carries the point: the &lt;code&gt;allowed&lt;/code&gt; set is yours to write, never the model's. The tempting shortcut is to let the agent write its own blast radius and have the hook read it back, but a boundary the model can edit is not a gate, it is one more instruction it can set aside. The set has to come from a human who knows what this task is allowed to touch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3w0zzmd4vflg70xvsng1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3w0zzmd4vflg70xvsng1.png" alt="A timeline. Early, at the write attempt, an in-loop guard blocks a write to a file outside the task's scoped paths and the write never lands. Much later, once the whole change is written and the pull request opens, a CI scope check fails on the same rule, after eleven files already changed." width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two honest limits sit on this gate. It matches the file-editing tools, &lt;code&gt;Write&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt;, and &lt;code&gt;MultiEdit&lt;/code&gt;; an agent that writes by shelling out, a &lt;code&gt;sed -i&lt;/code&gt; or a &lt;code&gt;&amp;amp;gt;&lt;/code&gt; redirect, goes around it until you put the same rule on a &lt;code&gt;Bash&lt;/code&gt; matcher or refuse writes from the shell. And it guards where the change lands, not what it does: the stray file it stops from being touched is the cheap failure, while a wrong line inside a file you did ask it to change is Layer two's job, never the gate's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advisory or blocking
&lt;/h2&gt;

&lt;p&gt;A gate has two settings, and the difference is whether it can be argued with. Swap the &lt;code&gt;exit 2&lt;/code&gt; for a printed line and &lt;code&gt;exit 0&lt;/code&gt;, and you have an advisory layer: it tells you the agent stepped outside the lines and trusts you to notice. Keep the &lt;code&gt;exit 2&lt;/code&gt; and you have a floor the model cannot route around. Advisory is the honest default while you are still learning where your own blast radius is, because a gate keyed too broadly refuses clean work, and a false block is work that cannot proceed until someone fixes the gate. Run it advisory first, watch what it would have blocked over a week, and promote the rule to a hard block once you trust it refuses only what you meant. A layer-three control that only warns is real work; the promotion to one that refuses is a separate decision you make once the evidence is in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What none of the three can do
&lt;/h2&gt;

&lt;p&gt;Be exact about the boundary, because this is easy to oversell. A gate checks the diff against a rule you wrote, and it has no model of reality to check against. Whether the caching layer was worth adding, whether the rename makes the next change cheaper or the codebase worse, whether the one-line fix was even the right fix for the bug you actually had: each of those needs a model of the world the change lives in, the product, the people using it, where the whole thing is headed. None of the three layers has that. Steering can ask for it and procedure can surface candidates, but the judgment itself stays with you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Finjpbxp6l6totolol0wu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Finjpbxp6l6totolol0wu.png" alt="Two panels. On the left, what the three layers settle, each with a definite answer: the change stayed inside the scoped paths, the format holds and no secret landed, the suite still fails when the code is broken. On the right, what none of them can settle: whether the caching layer was worth adding, whether the rename helps or hurts, whether it was even the right fix. Those stay with you." width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The enforcement layer is rarely one hook. It is a set of them you design for your own blast radius, your own must-hold constraints, the paths a change should never cross, the same move as writing the architecture rules you already keep for your codebase, &lt;a href="https://dev.to/cleverhoods/from-prompt-to-platform-architecture-rules-i-use-59gp"&gt;one level down&lt;/a&gt;. You are deciding, once and in advance, which of the agent's choices are yours to make and which it can make on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the rules once, gate the rest
&lt;/h2&gt;

&lt;p&gt;Three layers: the steering you write, the procedure you run, the gate you own. We run all three on our own agents, for the same reason you would: the agent is fast enough now that no single reader at the end of the day is the thing standing between its output and main.&lt;/p&gt;

&lt;p&gt;You reached for Opus 5 to write more and decide less. Pointing a second model at the first one feels like progress and quietly hands the deciding back to a guess. The way to actually spend less of your attention is to hand the machine-checkable parts to the machine: scope, format, a forbidden path, a test that no longer bites. What is left is the one question none of the three layers can answer, whether the change was worth making at all. That is the review the eleven-file diff still needs, and it was always the point. The layers do not shrink that review; they clear the noise around it, so the judgment is what is left on your desk instead of being buried under four hundred lines.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt;, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface you wrote down and tells you, with measured evidence, which rules couple to behavior and which are text the model can ignore.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>claude</category>
    </item>
    <item>
      <title>Opus 5: Review bottleneck</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:23:07 +0000</pubDate>
      <link>https://dev.to/reporails/opus-5-review-bottleneck-2c6p</link>
      <guid>https://dev.to/reporails/opus-5-review-bottleneck-2c6p</guid>
      <description>&lt;p&gt;The &lt;a href="https://youtu.be/qyPCVqFUyDo" rel="noopener noreferrer"&gt;pitch for Opus 5&lt;/a&gt; was that the model checks its own work now. Anthropic's Opus 5 guidance &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5" rel="noopener noreferrer"&gt;says as much&lt;/a&gt;: the model verifies by default, and the &lt;code&gt;verify&lt;/code&gt; and &lt;code&gt;double-check&lt;/code&gt; lines you used to write now only make it over-verify. Checking its own work as it writes is one job; reviewing the finished diff is another. Opus 5 got better at the first, and that quietly excuses you from the second: the four-hundred-line change it just produced lands on your branch behind a green check nobody actually read.&lt;/p&gt;

&lt;p&gt;If you run Claude Code with a few agents at once, you know the shape of it. One developer &lt;a href="https://news.ycombinator.com/item?id=49119987" rel="noopener noreferrer"&gt;on Hacker News&lt;/a&gt; put the volume plainly: "Once AI starts generating code, it flows out like water through a burst dam. It's impossible for any human to fully understand it all." Another, running the same setup I do: "I also fire off tons of parallel agents, and review is hands down the biggest bottleneck." (&lt;a href="https://news.ycombinator.com/item?id=45486217" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Opus 5 was supposed to make this faster. What it did was move the work. You write less and review more, and reviewing what a machine wrote is harder than reviewing your own, because none of the context is already in your head. Robert Laszczak &lt;a href="https://threedots.tech/post/understanding-code-is-bottleneck/" rel="noopener noreferrer"&gt;said it cleanly this month&lt;/a&gt;: "Reviewing code written by an agent is much harder than reviewing code that you have written by hand." You reached for a faster writer. You got a full-time reviewer of code you cannot fully hold in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  The green check is not a review
&lt;/h2&gt;

&lt;p&gt;When the queue backs up, the review stops being a review. &lt;a href="https://shmulc.substack.com/p/stop-vibe-merging" rel="noopener noreferrer"&gt;Shmulik Cohen&lt;/a&gt; has a name for what replaces it: vibe merging, "where developers, overwhelmed by the volume of AI-generated code, simply skim the diff or hit Approve based on a gut feeling." The tell they point to is the one you have already done: "the LGTM speedrun, approving a 300+ line diff in under three minutes." The green check did the reviewing. You just signed it.&lt;/p&gt;

&lt;p&gt;None of this is new. Developers skimmed diffs and waved through a quick LGTM long before agents wrote code. But a fifty-line changeset you skimmed, you mostly still caught; the skim was cheap because the change was small. The skim has not changed. The changeset has. Four hundred lines written in two minutes and approved in three, and the same gut-feel pass now lands on a fraction of what it used to cover. AI did not invent the rubber stamp. It multiplied what rides on it.&lt;/p&gt;

&lt;p&gt;This is not a private feeling, either. The industry instrumented it. &lt;a href="https://www.faros.ai/blog/ai-acceleration-whiplash-takeaways" rel="noopener noreferrer"&gt;Faros AI&lt;/a&gt; read two years of telemetry from 22,000 developers and found median time in code review up 441.5%, and pull requests merged with no review at all up 31.3%. &lt;a href="https://linearb.io/blog/8-million-prs-engineering-productivity" rel="noopener noreferrer"&gt;LinearB&lt;/a&gt;, across 8.1 million pull requests, found AI-assisted PRs run 2.6 times larger than hand-written ones and sit more than five times longer before a reviewer even picks them up. The code arrives faster; the reading does not. And the quality gap underneath it is one developers can feel: in &lt;a href="https://survey.stackoverflow.co/2025/ai" rel="noopener noreferrer"&gt;Stack Overflow's 2025 survey&lt;/a&gt;, 46% now distrust the accuracy of AI output against 33% who trust it, only 3% highly, and the single biggest frustration, cited by 66%, is the answer that is "almost right, but not quite." That is precisely the kind of bug that survives a three-minute skim.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdk4y6o8fzy1zusoz2avb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdk4y6o8fzy1zusoz2avb.png" alt="Measured figures on AI code review: median time in code review up 441.5% and pull requests merged with no review up 31.3% (Faros AI, two years of telemetry from 22,000 developers); AI-assisted pull requests 2.6 times larger and waiting more than five times longer for a reviewer to pick them up (LinearB, 8.1 million pull requests); and 46% of developers distrusting AI accuracy against 33% trusting it, the top frustration being answers that are almost right but not quite, at 66% (Stack Overflow 2025)." width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The reflex is to point a model at the model
&lt;/h2&gt;

&lt;p&gt;So the obvious move is to automate the reading. If a model wrote the code, have a model review the code. Opus 5 will tell you to do exactly this. Ask it how to keep its own mistakes out of main and it will happily offer to review its own diffs, the guess volunteering to grade the guess. That is the recommendation to distrust first. Every code host now ships an AI reviewer that leaves comments on the diff, and for the mechanical layer it genuinely helps, the way a linter helps: it "generates the same sort of local, mechanical feedback a linter does, all the stuff that might bog down a human reviewer and keep them from handling the big-picture items." (&lt;a href="https://news.ycombinator.com/item?id=42487707" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The trouble starts when you ask that reviewer to be the thing standing between a four-hundred-line AI diff and your main branch. Now you have a model checking a model, and its verdict is the same kind of stochastic guess that wrote the code, made on a question that gets harder, not easier, the more code you hand it. Across a large change the odds of being right on every call multiply down: a per-line accuracy that looks fine on one function collapses across a hundred of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stronger base model does not rescue you
&lt;/h2&gt;

&lt;p&gt;It is still a guess, and the number of guesses is what grows. The argument is the same one from &lt;a href="https://reporails.com/articles/opus-5-delete-your-claudemd" rel="noopener noreferrer"&gt;Opus 5: Delete your CLAUDE.md?&lt;/a&gt;, where the model grades its own instruction rules: a stochastic judge run many times over gets less reliable exactly where you leaned on it hardest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftllsg83ra61cs9i7613j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftllsg83ra61cs9i7613j.png" alt="Two review paths over a four-hundred-line AI diff: a second model grading the first, its per-line accuracy multiplying down toward zero as the diff grows, against a deterministic gate that forms no opinion and returns the same verdict on the hundredth file as on the first." width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually holds the line
&lt;/h2&gt;

&lt;p&gt;The thing that scales with the model is not a faster reader but a gate that does not read at all. A deterministic check refuses an action outright: a test that fails, a lint rule that blocks, a hook that denies a write to a path the change was never supposed to touch, a schema that rejects a malformed file. It forms no opinion about the diff, so it does not get less reliable as the diff gets bigger, and it returns the same verdict on the hundredth file as on the first. &lt;a href="https://reporails.com/articles/deterministic-guardrails-prompts-steer-hooks-enforce" rel="noopener noreferrer"&gt;Prompts steer, hooks enforce&lt;/a&gt;: steering is advice the model can weigh and set aside, a gate is a floor it cannot. We run gates like this on our own agents, and they catch what a tired reviewer at 6pm does not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyfvjp2659zkf59mroi63.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyfvjp2659zkf59mroi63.png" alt="Workflow" width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bryanfinster.substack.com/p/ai-broke-your-code-review-heres-how" rel="noopener noreferrer"&gt;Bryan Finster&lt;/a&gt;, who has argued for years that code review was always the weak point, lands in the same place: "The answer is to automate all the things that can be automated, and reserve human judgment for what genuinely requires it."&lt;/p&gt;

&lt;h2&gt;
  
  
  What a gate cannot do
&lt;/h2&gt;

&lt;p&gt;It is worth being exact about the boundary, because this is easy to oversell. Judging whether this is the right feature, whether the architecture will still hold in six months, whether the code does what the ticket actually meant: each of those needs a model of the world the change lives in, the product, the people using it, where the whole thing is headed. A gate has none of that. It checks the diff against a rule, never against reality, so that call stays human.&lt;/p&gt;

&lt;p&gt;And some of the pushback on the whole "review is the new bottleneck" framing is warranted. One developer's &lt;a href="https://news.ycombinator.com/item?id=44429789" rel="noopener noreferrer"&gt;flat response to the panic&lt;/a&gt; was "was anyone claiming it is the bottleneck? Seems like a straw man," and they are right that review was always a constraint. What changed is the ratio. When you wrote the code, your judgment went mostly into writing and a little into review. Now the writing is nearly free and the judgment is the whole job, so the mechanical load that used to hide inside your review has to move somewhere the model cannot talk its way past. That somewhere is a gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the rules once, gate the rest
&lt;/h2&gt;

&lt;p&gt;You reached for Opus 5 to write more and decide less. The way to actually get that is to stop spending human attention on the parts a check can settle, format, conflicts, a forbidden call, a write outside the blast radius, and spend it on the one thing a model cannot hand you, which is whether this was worth building at all. Pointing a second model at the first one feels like progress and quietly rebuilds the problem one guess at a time. A gate the model cannot argue with is the boring answer, and it is the one still standing on the hundredth pull request of the day.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt;, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface you wrote down and tells you, with measured evidence, which instructions couple to behavior and which are text the model can ignore. It does not run your model, and it does not vote; it measures the file.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Opus 5: How to Fix Verbose Output</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Fri, 14 Aug 2026 18:17:39 +0000</pubDate>
      <link>https://dev.to/reporails/opus-5-how-to-fix-verbose-output-4amn</link>
      <guid>https://dev.to/reporails/opus-5-how-to-fix-verbose-output-4amn</guid>
      <description>&lt;p&gt;You might have noticed that Opus 5 is extremely eager to talk. It is like the uncle at the family party who always has an overly long, boring story about literally anything, and no request is too small to set one off. &lt;/p&gt;

&lt;p&gt;Ask said uncle for a one-line change and you'll get the long history of why the code needed changing, why he never spotted it sooner, how it was not his change in the first place, and how this whole thing reminds him of a story... which was the right call, mind you, and here is how you might approach the problem next time. Your &lt;code&gt;CLAUDE.md&lt;/code&gt; has said &lt;code&gt;Be concise. No preamble.&lt;/code&gt; near the top for a year. On 4.x it mostly listened. On 5 the line is still sitting there, in the same file, in the same position, and the model reads straight past it.&lt;/p&gt;

&lt;p&gt;I gave someone four paths for this in a Reddit thread. Here they are again, weakest lever first.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLAUDE.md instructions
&lt;/h2&gt;

&lt;p&gt;Write the rule down first. It is the obvious move and the weakest one. &lt;code&gt;Be concise&lt;/code&gt; on its own barely registers, because it names nothing the model can act on. A rule that names the shape you want does better. Here is the one I use for reply verbosity, paste-ready for your own &lt;code&gt;CLAUDE.md&lt;/code&gt;:&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;## Reply shape&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Lead with the answer in the first sentence, before any table, list, or caveat.
&lt;span class="p"&gt;-&lt;/span&gt; Carry status, comparisons, and any multi-item result in a &lt;span class="sb"&gt;`table`&lt;/span&gt; or &lt;span class="sb"&gt;`bullet`&lt;/span&gt; list, never a prose paragraph.
&lt;span class="p"&gt;-&lt;/span&gt; Cap unbroken prose at two paragraphs; if a third starts, convert the run to a &lt;span class="sb"&gt;`bullet`&lt;/span&gt; list or &lt;span class="sb"&gt;`table`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Give each item its own &lt;span class="sb"&gt;`bullet`&lt;/span&gt; row, not a clause buried across sentences.
&lt;span class="p"&gt;-&lt;/span&gt; Compose in this shape from the start; do not draft prose and reshape it afterward.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat to keep in mind: a short confirmation is already the answer. &lt;code&gt;Done.&lt;/code&gt; or &lt;code&gt;Yes, that works.&lt;/code&gt; should not get tabulated into a wall. The rule is about shape when there is something to shape.&lt;/p&gt;

&lt;p&gt;Even written this tightly, a &lt;code&gt;CLAUDE.md&lt;/code&gt; line is advice, and it competes against everything else the model is holding. On Opus 5 there is more of that than there used to be. I went into why a line that worked on 4.x can go quiet on 5 in a separate piece, &lt;a href="https://reporails.com/articles/opus-5-delete-your-claudemd" rel="noopener noreferrer"&gt;Opus 5: Delete your CLAUDE.md?&lt;/a&gt;: the model reaches for more of your rules at once and checks its own work by default, so even a sharp rule gets crowded out. Write the rule, but do not expect it to hold on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output styles
&lt;/h2&gt;

&lt;p&gt;An output style is a step up, and the reason is where it lands. A &lt;code&gt;CLAUDE.md&lt;/code&gt; file rides in as a user message after the system prompt; an output style modifies the system prompt itself, and Claude Code adds its instructions to the end of that prompt (per the &lt;a href="https://code.claude.com/docs/en/output-styles" rel="noopener noreferrer"&gt;output-styles docs&lt;/a&gt;). Same kind of instruction, but it travels through the session better. Being in the system prompt, it is not one more user-turn line competing with everything else you have said, and the docs note Claude Code re-reminds the model to follow the active style during the conversation, so the rule gets re-surfaced rather than decaying after one appearance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fewss6awmnx0q96mrfjdi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fewss6awmnx0q96mrfjdi.png" alt="A comparison of where a reply-shape rule lands in the model's context: as an output style it is appended to the end of the system prompt and re-surfaced every turn, while as a CLAUDE.md line it rides in a user message after the system prompt, competing with everything else." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can switch to a terser built-in style, or write your own. To turn the reply-shape rule from the last section into a style, save it at &lt;code&gt;.claude/output-styles/reply-shape.md&lt;/code&gt; and give it a frontmatter header. The header is the part that makes it an output style rather than plain text:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reply shape&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer-first,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scannable&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;replies;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tables&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;bullets&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;over&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prose&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;walls"&lt;/span&gt;
&lt;span class="na"&gt;keep-coding-instructions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

... the instructions from the first point ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same five reply-shape lines from the last section go in the body below that header. &lt;code&gt;keep-coding-instructions: true&lt;/code&gt; keeps Claude Code's built-in engineering behavior and changes only how it communicates. Then pick the style from &lt;code&gt;/config&lt;/code&gt; under Output style. (The standalone &lt;code&gt;/output-style&lt;/code&gt; command was removed; it lives under &lt;code&gt;/config&lt;/code&gt; now.) It is read once at session start, so a change takes effect after &lt;code&gt;/clear&lt;/code&gt; or a new session. Still instruction, so the model can still drift, but it drifts less than from a line buried in a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hook that gates the output
&lt;/h2&gt;

&lt;p&gt;The rule and the style are both instruction, and the model can weigh instruction and set it aside. A hook is different. It is a script the harness runs at a fixed moment, and it can refuse. That makes it the strongest of the three levers, and the one worth showing in full.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyunpmvfppwoxjikcvvu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyunpmvfppwoxjikcvvu.png" alt="A flow of the Stop hook: the model finishes a turn, the hook reads the reply on stdin as last_assistant_message and checks whether its word count is over the limit; over budget, exit 2 blocks the stop and sends the reply back to be rewritten shorter, capped at five blocks in a row; within budget, exit 0 lets the turn end." width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code fires a &lt;code&gt;Stop&lt;/code&gt; hook the moment the model finishes a turn, and hands the hook the finished reply on stdin as &lt;code&gt;last_assistant_message&lt;/code&gt;, so it can read what was just written. If the reply runs long, exit 2 blocks the stop and sends a line back to the model telling it to answer again, shorter. Drop this in &lt;code&gt;.claude/hooks/gate-length.sh&lt;/code&gt; and make it executable:&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;# Stop hook: if the reply ran long, send it back once to tighten up.&lt;/span&gt;

&lt;span class="c"&gt;# The Stop event hands us the finished reply on stdin.&lt;/span&gt;
&lt;span class="nv"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.last_assistant_message // ""'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Your bar. Word count here; a line count or a preamble check works the same way.&lt;/span&gt;
&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;180
&lt;span class="nv"&gt;words&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&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;$reply&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-w&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="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;$words&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;$limit&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="c"&gt;# Exit 2 blocks the stop; this line goes back to the model as its instruction.&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Your reply ran &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;words&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; words; the budget is &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Rewrite it under &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; words: put the answer in the first sentence, then cut the preamble, the recap, and the summary of what you did."&lt;/span&gt; &amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;&amp;amp;amp&lt;span class="p"&gt;;&lt;/span&gt;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then wire it in &lt;code&gt;.claude/settings.json&lt;/code&gt;. A &lt;code&gt;Stop&lt;/code&gt; hook takes no matcher:&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;"hooks"&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;"Stop"&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;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&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_PROJECT_DIR}/.claude/hooks/gate-length.sh"&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="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;Change &lt;code&gt;limit&lt;/code&gt;, or swap the word count for a line count or a grep for opening filler. It cannot loop forever: Claude Code caps a &lt;code&gt;Stop&lt;/code&gt; hook at five consecutive blocks with no tool call between them, then lets the turn end.&lt;/p&gt;

&lt;p&gt;We run a version of this on our own agents. The system that writes and reviews the Reporails codebase has a &lt;code&gt;Stop&lt;/code&gt;-boundary check that reads each reply and sends it back when it runs too verbose or breaks format. It fired on the drafts of this article more than once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom plugins
&lt;/h2&gt;

&lt;p&gt;By now you have three levers: a rule, a style, and a hook. A plugin is how you stop rebuilding them. It bundles any of the three so the setup travels with you across projects instead of getting re-pasted into each repo, and a Claude Code plugin can carry hooks, output styles, and rules together. It adds no strength of its own; the job is portability.&lt;/p&gt;

&lt;p&gt;To make one, drop a &lt;code&gt;.claude-plugin/plugin.json&lt;/code&gt; manifest next to your &lt;code&gt;hooks/&lt;/code&gt; and rules, test it with &lt;code&gt;claude --plugin-dir ./your-plugin&lt;/code&gt;, then install it, yours or someone else's, from a marketplace with &lt;code&gt;/plugin install&lt;/code&gt;. Anthropic's &lt;a href="https://code.claude.com/docs/en/plugins" rel="noopener noreferrer"&gt;plugins docs&lt;/a&gt; carry the manifest fields and the marketplace steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one to reach for
&lt;/h2&gt;

&lt;p&gt;Reach for the least you can get away with. A &lt;code&gt;CLAUDE.md&lt;/code&gt; rule for the parts a human reads too. An output style when you want the model leaning terser by default. The hook when you want a floor on length the model cannot talk its way past. A plugin once you are tired of setting those up again in every repo.&lt;/p&gt;

&lt;p&gt;The three levers are one move underneath: they decide what the model is holding when it answers, and whether anything checks the answer after it lands. The plugin only carries them. Reply length is the version of that you notice first. The harder version is which of your rules the model actually follows once you have written a hundred of them that quietly disagree, and that one is worth its own piece. It is the one I am writing next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work on &lt;a href="https://github.com/reporails/cli" rel="noopener noreferrer"&gt;Reporails&lt;/a&gt;, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface you wrote down and diagnoses why your steering drifts, with measured evidence: which instructions couple to behavior, which name nothing the model can bind to, and where two rules cannot both hold. It does not run your model, and it does not vote; it measures the file.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
