<?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:  Gábor Mészáros</title>
    <description>The latest articles on DEV Community by  Gábor Mészáros (@cleverhoods).</description>
    <link>https://dev.to/cleverhoods</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3647906%2F2ae4010e-7f1a-4906-9598-c259abb6e222.jpeg</url>
      <title>DEV Community:  Gábor Mészáros</title>
      <link>https://dev.to/cleverhoods</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cleverhoods"/>
    <language>en</language>
    <item>
      <title>State of AI Instruction Quality: Repos Split on Co-Authored-By</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:40:48 +0000</pubDate>
      <link>https://dev.to/reporails/state-of-ai-instruction-quality-repos-split-on-co-authored-by-5bfl</link>
      <guid>https://dev.to/reporails/state-of-ai-instruction-quality-repos-split-on-co-authored-by-5bfl</guid>
      <description>&lt;p&gt;The 10,000 most-starred repos split on whether their coding agent should sign its commits.&lt;/p&gt;

&lt;p&gt;About 400 tell it to leave its name off. One of the most-starred web frameworks puts it in its &lt;code&gt;CLAUDE.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do NOT add "Generated with Claude Code" or co-author footers to commits or PRs&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;About 330 ask for the opposite: an &lt;code&gt;Assisted-by:&lt;/code&gt; line or an AI co-author trailer, often inside a commit template the agent is told to follow. And about 30 of the repos that rule out the co-author line ask for &lt;code&gt;Assisted-by:&lt;/code&gt; in its place.&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%2F4ppw46nq06hucc0ppq2g.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%2F4ppw46nq06hucc0ppq2g.png" alt="Repos in the Q3 report whose instruction files mention an AI commit trailer: 411 only tell the agent not to add one, 273 only ask for one, and 54 do both, often " width="799" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rules go after
&lt;/h2&gt;

&lt;p&gt;Mostly the trailer. More than half of the matches in the 465 repos with a rule against it name &lt;code&gt;Co-Authored-By&lt;/code&gt; itself. In 295 of them the rule mentions commits, and in 112 it mentions pull requests. Two-thirds of the matches sit in main files like &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;AGENTS.md&lt;/code&gt;, which load in every session.&lt;/p&gt;

&lt;p&gt;Most of these rules name no agent at all: 283 of the 465 repos write "no co-author trailer" or "no AI attribution" in general. 146 name only Claude. 36 name another agent, most often Codex, then Copilot and Cursor:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No &lt;code&gt;Co-authored-by: Copilot&lt;/code&gt; or any other co-author line. Ever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The vendor-neutral file carries Claude-specific rules too. 77 of the 273 matches in &lt;code&gt;AGENTS.md&lt;/code&gt; name Claude.&lt;/p&gt;

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

&lt;p&gt;A rule about commit trailers matters once the agent makes commits, and the repos that write one run bigger, more built-out setups. Their median repo has 4 instruction files, against 2 across the report. 60% write for more than one agent, against 43%, and 17% ship an agent hook, against 7.6%.&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%2F7nn1rhfmx7p0gyo4uiea.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%2F7nn1rhfmx7p0gyo4uiea.png" alt="Repos with an attribution rule against all 9,784 repos: write for more than one agent 60.0% vs 43.1%; have skills 42.2% vs 25.3%; ship an agent hook 17.0% vs 7.6%; have sub-agents 14.4% vs 5.9%; forbid destructive commands 12.7% vs 3.7%." width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They write firmer instructions too: 4.2 &lt;a href="https://reporails.com/rules/core/modality-weakness" rel="noopener noreferrer"&gt;hedged instructions&lt;/a&gt; per 100 ("should", "try to") against 5.1, lower at every repo size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some want a different disclosure
&lt;/h2&gt;

&lt;p&gt;137 repos ask for an &lt;code&gt;Assisted-by:&lt;/code&gt; trailer, and about 30 of the repos that rule out the co-author line say to use &lt;code&gt;Assisted-by:&lt;/code&gt; instead. The Rust compiler repo, &lt;code&gt;rust-lang/rust&lt;/code&gt;, has both in one passage of its &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;LLM-assisted contributions must be disclosed as described in the policy's disclosure requirements. Lying about or concealing LLM use is a Code of Conduct violation. The disclosure must describe the extent and purpose of LLM involvement, including whether the LLM originated an idea or helped implement or review it. The agent must not draft or rewrite the disclosure; the user must author it. Do NOT add &lt;code&gt;Co-Authored-By&lt;/code&gt; trailers to commits.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its last sentence reads like the framework's rule. The sentences above it give the reason: the project wants a person to write the disclosure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trailer is a default
&lt;/h2&gt;

&lt;p&gt;Claude Code adds it unless told otherwise. Its settings docs: "Commits get a git trailer such as &lt;code&gt;Co-Authored-By&lt;/code&gt; by default; pull request descriptions get plain text." Cursor's agents add a "Made with Cursor" trailer to commits and pull requests, on by default, with a switch in its settings and one for team admins. Cursor's staff add on its forum that a rule won't remove it, because the trailer "is added by Cursor at the tooling level, not by the model".&lt;/p&gt;

&lt;p&gt;Cloud agents reverse the roles. GitHub's docs: Copilot cloud agent's commits "are authored by Copilot, with the developer who assigned the issue or requested the change to the pull request marked as the co-author". Cursor's cloud agents commit as Cursor Agent and add the person who started the session the same way, and per Cursor's staff that can't be turned off yet.&lt;/p&gt;

&lt;p&gt;In Claude Code, a setting turns it off. In the project's &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;"attribution"&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;"commit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sessionUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;That's the form repos use: 49 set &lt;code&gt;commit&lt;/code&gt; to an empty string and 44 set &lt;code&gt;pr&lt;/code&gt;. Claude Code v2.1.281 and later also take &lt;code&gt;"attribution": false&lt;/code&gt;, but earlier versions reject that value and skip the whole settings file that holds it. The strings can also carry your own text, such as an &lt;code&gt;Assisted-by:&lt;/code&gt; line.&lt;/p&gt;

&lt;p&gt;64 repos turn attribution off in a committed setting, and 54 of them write no rule about it at all. Of the 465 with a rule, 10 also use the setting.&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%2Ffeebdvnptsfvtdoogwbx.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%2Ffeebdvnptsfvtdoogwbx.png" alt="Repos that turn off the agent's commit attribution, 519 in all: 455 with a rule in an instruction file only, 10 with a rule and a Claude Code setting, and 54 with the setting only. Personal ~/.claude settings are not in the repo and are not counted." width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rule works too. The docs say "Claude Code tells Claude that your own instructions about attribution, such as a CLAUDE.md or memory rule, take precedence over these commit and PR lines". The setting changes what Claude Code adds in the first place, so it holds whether or not the agent follows the rule. Settings can also live in each developer's own &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, which a repo snapshot can't see.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hook catches the rest
&lt;/h2&gt;

&lt;p&gt;A git &lt;code&gt;commit-msg&lt;/code&gt; hook runs after the agent, or the tool behind it, has written the message, so it holds for every agent that commits on your machine. Some repos already run one ("the commit-msg hook will reject it", one &lt;code&gt;CLAUDE.md&lt;/code&gt; warns), and Cursor's staff point to it as the workaround for the trailer its tooling adds. This one deletes AI attribution lines and keeps human co-authors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .githooks/commit-msg&lt;/span&gt;
&lt;span class="nv"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'^(co-authored-by|made-with):.*(claude|copilot|cursor|codex|gemini|anthropic\.com)|generated with \[?claude code'&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;-viE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit it, make it executable, and run &lt;code&gt;git config core.hooksPath .githooks&lt;/code&gt; once per clone. It covers commits only, so pull request text still needs the settings, and it can't reach cloud agents, which commit on the vendor's machines. In Claude Code, a &lt;code&gt;PreToolUse&lt;/code&gt; hook can also refuse a commit command that carries the trailer before it runs; &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 how.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rule every agent reads
&lt;/h2&gt;

&lt;p&gt;The setting is Claude Code's. A rule in &lt;code&gt;AGENTS.md&lt;/code&gt; reaches every agent that loads the file, which is the reason to keep one when the repo serves more than one agent. Where the tool adds the trailer itself, as in Cursor, the rule states the policy and the setting or the hook carries it out. Written with the directives first, then context, and the constraint last, it could read:&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="p"&gt;-&lt;/span&gt; Write commit messages in the format described in &lt;span class="sb"&gt;`CONTRIBUTING.md`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Add an &lt;span class="sb"&gt;`Assisted-by:`&lt;/span&gt; trailer only with text the user gives you.
&lt;span class="p"&gt;-&lt;/span&gt; The project records AI assistance through the person committing the change.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="ge"&gt;*Never add your own name to a commit or pull request.*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraint is in italics and rules out the trailer without naming it. In a &lt;code&gt;CLAUDE.md&lt;/code&gt; of their own, these lines score 9.4 on &lt;code&gt;ails check&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;The checker scores an attribution rule like any other instruction. The framework's rule quoted at the top draws a finding for &lt;a href="https://reporails.com/rules/core/italic-constraints" rel="noopener noreferrer"&gt;a constraint written outside italics&lt;/a&gt;. 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
npx @reporails/cli check &lt;span class="nt"&gt;--agent&lt;/span&gt; codex|cursor|claude &lt;span class="c"&gt;#whichever you are using&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find every attribution line, including rules that ask for a trailer and the &lt;code&gt;attribution&lt;/code&gt; setting itself:&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;-niE&lt;/span&gt; &lt;span class="s2"&gt;"co-?authored|assisted-by|attribution|generated (by|with)"&lt;/span&gt; CLAUDE.md AGENTS.md .claude/settings.json 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read what comes back as one policy: what the agent may sign, where, and whether it lives in a sentence the agent reads or a setting it can't skip.&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. The data behind the series is 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;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A rule against the trailer counts when a line in an instruction file, outside code blocks, has "never", "do not", "don't", "avoid" or "no" followed within 50 characters by "co-authored", "attribution", "mention of Claude/AI/Anthropic" or "generated by/with Claude/AI": 465 of the 9,784 repos with a setup. In a hand-read sample of 40 distinct matching lines, 35 were AI-attribution rules; the rest used "attribution" in another sense, such as data or citations, so "about 400" stands for the 465. An ask counts when an instruction or a commit template in an instruction file asks for an &lt;code&gt;Assisted-by:&lt;/code&gt; or AI co-author trailer: 327 repos, with 28 of 30 hand-checked asks real, so "about 330". Settings counts read the &lt;code&gt;.claude/settings.json&lt;/code&gt; and &lt;code&gt;.claude/settings.local.json&lt;/code&gt; files committed to the repo, 978 files in 959 repos.&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: Files With Persona Lines Score Lower</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Mon, 19 Oct 2026 12:53:27 +0000</pubDate>
      <link>https://dev.to/reporails/state-of-ai-instruction-quality-files-with-persona-lines-score-lower-3dff</link>
      <guid>https://dev.to/reporails/state-of-ai-instruction-quality-files-with-persona-lines-score-lower-3dff</guid>
      <description>&lt;p&gt;Main instruction files that tell the agent who it is score lower than the rest, at every file size.&lt;/p&gt;

&lt;p&gt;One of them opens like this, on line 3 of its &lt;code&gt;CLAUDE.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are a senior [project] engineer working in a Yarn/Turbo monorepo. You prioritize type safety, security, and small, reviewable diffs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a familiar move from prompt writing. In the 10,000 most-starred repos of &lt;a href="https://reporails.com/articles/state-of-ai-instruction-quality-q3-2026" rel="noopener noreferrer"&gt;the Q3 report&lt;/a&gt;, 285 main instruction files have a line that starts "You are a", "You are an" or "You are the", across &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;GEMINI.md&lt;/code&gt;, Copilot's &lt;code&gt;copilot-instructions.md&lt;/code&gt; and Cursor's &lt;code&gt;.cursorrules&lt;/code&gt;. On &lt;code&gt;ails check&lt;/code&gt;'s 0 to 10 scale, their median is 6.6, against 7.7 for the other 20,551 main files. Fewer than one in five score 8 or above. Of the rest, more than four in ten do.&lt;/p&gt;

&lt;p&gt;Persona files tend to be longer, so we split all main files into four equal groups by size. The gap holds in every group, from 0.9 to 1.25 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%2Fac20zhw3dzi52z0t1v4p.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%2Fac20zhw3dzi52z0t1v4p.png" alt="Median main-file score with and without a " width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A file that reads like a prompt
&lt;/h2&gt;

&lt;p&gt;The "You are" sentence itself reads as description to the checker. The difference is in the rest of the file. Nearly half of what a persona file says is instruction (46.6%), against 30.3% in other main files, which spend more of their lines on the repo. After the "You are" comes a list of what the agent does and doesn't do.&lt;/p&gt;

&lt;p&gt;Compared with main files of the same size, that list picks up more of most kinds of finding. In the third size group (68 to 127 items), 80% of persona files have 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;, against 49% of the others. 78% have a &lt;a href="https://reporails.com/rules/core/modality-weakness" rel="noopener noreferrer"&gt;hedged instruction&lt;/a&gt; ("should", "try to"), against 60%. 59% put their instructions in &lt;a href="https://reporails.com/rules/core/instruction-ordering" rel="noopener noreferrer"&gt;the weaker order&lt;/a&gt;, against 40%, 45% have &lt;a href="https://reporails.com/rules/core/heading-as-instruction" rel="noopener noreferrer"&gt;a heading written as an instruction&lt;/a&gt;, against 28%, and 25% put &lt;a href="https://reporails.com/rules/core/formatting-regime" rel="noopener noreferrer"&gt;the forbidden thing in bold&lt;/a&gt; inside a prohibition, against 10%.&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%2Fcsdmmc7k5qdvl2ybbgx3.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%2Fcsdmmc7k5qdvl2ybbgx3.png" alt="Share of main files with each finding, files of similar size: a never outweighing its permission 80% of " width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "never" gap holds in all four size groups. In the largest, it reaches 98% of persona files against 75% of the rest. Persona files also carry more prohibitions in the first place: 88% have at least one, against 78%.&lt;/p&gt;

&lt;p&gt;The smallest persona files go wrong in a different way. 71% have an instruction &lt;a href="https://reporails.com/rules/core/instruction-elaboration" rel="noopener noreferrer"&gt;too short to register&lt;/a&gt;, such as "Be concise", against 48% of small files without the line.&lt;/p&gt;

&lt;p&gt;One finding runs the other way. Persona files have less &lt;a href="https://reporails.com/rules/core/content-dilution" rel="noopener noreferrer"&gt;prose crowding an instruction&lt;/a&gt;, 4.2 per 100 instructions against 9.9, which fits files that carry less description.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the line sits
&lt;/h2&gt;

&lt;p&gt;In 204 of the 285 files, the "You are" line is in the first five lines, so it's how the file opens. Of setups with a main file, 6.0% of Cursor's have the line there, against 3.5% of Copilot's and 1.0% of Claude Code's.&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%2Fcnxtprb1sjduaszxea26.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%2Fcnxtprb1sjduaszxea26.png" alt="Share of setups whose main instruction file has a " width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code's docs say where a &lt;code&gt;CLAUDE.md&lt;/code&gt; ends up: "CLAUDE.md content is delivered as a user message after the system prompt, not as part of the system prompt itself." By the time the file arrives, the agent already has its system prompt.&lt;/p&gt;

&lt;p&gt;Sub-agents are different. A Claude Code sub-agent file is YAML frontmatter "followed by the system prompt in Markdown", and the docs' own examples open with a persona line, such as "You are a senior code reviewer…". 57.4% of sub-agent files in the report have one. There it's the convention, and this post leaves it alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to write instead
&lt;/h2&gt;

&lt;p&gt;Ask what the persona line was meant to buy, and write that down. "Senior engineer who prioritizes type safety" can mean running the type checker and keeping the types strict. Each of those can name its tool, one instruction per sentence: the directives first, a line of context, and the constraint last. For the Yarn/Turbo monorepo above, that could read:&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="p"&gt;-&lt;/span&gt; Run &lt;span class="sb"&gt;`yarn type-check`&lt;/span&gt; before you mark a task as done.
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`unknown`&lt;/span&gt; and narrow it when a value's type is genuinely unknown.
&lt;span class="p"&gt;-&lt;/span&gt; Strict types are how this repo catches bugs before review.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="ge"&gt;*Never loosen a type just to get the build passing.*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraint is in italics and comes after the line that says what to do instead. It describes the move it rules out without naming &lt;code&gt;any&lt;/code&gt; or &lt;code&gt;@ts-ignore&lt;/code&gt;, so neither shows up in the agent's instructions (&lt;a href="https://reporails.com/articles/do-not-think-pink-elephant" rel="noopener noreferrer"&gt;why that matters&lt;/a&gt;). In a &lt;code&gt;CLAUDE.md&lt;/code&gt; of their own, these lines score 9.4 on &lt;code&gt;ails check&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;The checker flags what the persona files carry: &lt;a href="https://reporails.com/rules/core/heading-as-instruction" rel="noopener noreferrer"&gt;a heading written as an instruction&lt;/a&gt;, &lt;a href="https://reporails.com/rules/core/modality-weakness" rel="noopener noreferrer"&gt;a hedged "should" or "try to"&lt;/a&gt;, &lt;a href="https://reporails.com/rules/core/formatting-regime" rel="noopener noreferrer"&gt;bold on the thing a prohibition forbids&lt;/a&gt; and &lt;a href="https://reporails.com/rules/core/instruction-elaboration" rel="noopener noreferrer"&gt;a one-line trait like "Be concise"&lt;/a&gt;. 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
npx @reporails/cli check &lt;span class="nt"&gt;--agent&lt;/span&gt; copilot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find the line itself:&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;-niE&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*You are (a|an|the) "&lt;/span&gt; CLAUDE.md AGENTS.md GEMINI.md .github/copilot-instructions.md .cursorrules 2&amp;gt;/dev/null
&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;"Main files" are &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;copilot-instructions.md&lt;/code&gt;, &lt;code&gt;GEMINI.md&lt;/code&gt; and &lt;code&gt;.cursorrules&lt;/code&gt;, each file counted once even when more than one agent's setup loads it: 20,836 files, with the 285 persona files in 189 repos. A file counts when a line outside code blocks starts with "You are a", "You are an" or "You are the". Size groups are quarters of main files by item count, cut at 32, 67 and 127 items; finding shares compare files in the same group. The hedged, ordering and bold-prohibition gaps hold in three of the four groups, and the smallest group ties or reverses.&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>Claude Code mods: Brickfall, a brick breaker that's loud without getting in the way</title>
      <dc:creator> Gábor Mészáros</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:51:23 +0000</pubDate>
      <link>https://dev.to/reporails/claude-code-mods-brickfall-a-brick-breaker-thats-loud-without-getting-in-the-way-2kfa</link>
      <guid>https://dev.to/reporails/claude-code-mods-brickfall-a-brick-breaker-thats-loud-without-getting-in-the-way-2kfa</guid>
      <description>&lt;p&gt;After the &lt;a href="https://dev.to/reporails/claude-code-mods-minesweeper-and-testkit-3067"&gt;Minesweeper&lt;/a&gt; and &lt;a href="https://dev.to/reporails/claude-code-mods-but-does-it-run-doom-2mma"&gt;Doom&lt;/a&gt;, the third game in the arcade is a brick breaker after Arkanoid. The brief was one word: louder. Sparkle, big numbers, things raining down, a backdrop that shimmers. Brickfall plays in a pane beside the transcript 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%2Fg338cc6ulbil07bk1lmh.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%2Fg338cc6ulbil07bk1lmh.png" alt="Brickfall in a Claude Code pane while Claude documents a small project and runs its tests" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/bricks&lt;/code&gt; opens the pane. In kitty or Ghostty the field is a real picture; anywhere else it is drawn in quadrant block characters, four pixels a cell. You steer with the mouse, and there are thirteen kinds of capsule (laser, split, lightning, a giant ball, a shield of light...), gems that build toward a FEVER of double points, volt bricks that blow up their neighbours in chains, and ten rounds that come round again faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loud, but never in the way
&lt;/h2&gt;

&lt;p&gt;Every hit lands: light bursts where the brick was, the brick swells and burns out, the walls flash its colour, and a big number flies off it. My first loud version also shook the screen. It looked great in a screenshot and was miserable to play, because you can't catch a ball that jumps around with the field. The shake went, and two rules stayed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nothing moves the field or stops the play, however much is going off&lt;/li&gt;
&lt;li&gt;the ball, the paddle and anything falling are drawn on top of every effect, so what you need to catch stays in sight&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Every pixel goes through Claude Code
&lt;/h2&gt;

&lt;p&gt;Doom's frames never touch Claude Code: the engine writes a file and Claude Code hands kitty the path. Brickfall has no engine. The game runs inside the mod, so each frame is RGBA bytes handed to an &lt;code&gt;Image&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rgba&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toRgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&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;rgba&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;base64&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;rgba&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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes sharpness a budget. kitty stretches the picture to fill the pane and smooths it as it goes, so the picture is only as sharp as the pixels you send, and every pixel crosses into the terminal on every frame. Brickfall sends about 58,000 a frame:&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;const&lt;/span&gt; &lt;span class="nx"&gt;IMAGE_PIXELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;36000&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;QUALITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.25&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="mf"&gt;0.8&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a busy machine it gives up a little sharpness, never below half (about 29,000), and only when frames stay slow, so a burst of fireworks never costs it. It wins the sharpness back as soon as the sharper step would still draw in time:&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;fits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sharper&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sharper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;here&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;FRAME_BUDGET_MS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;FRAME_BUDGET_MS&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;pace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slow&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="mi"&gt;0&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quick&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="nx"&gt;pace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quick&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="mi"&gt;0&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;slow&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;PACE_FRAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;down&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;QUALITY&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;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&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="na"&gt;slow&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;quick&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;PACE_FRAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;up&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;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&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="na"&gt;slow&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;quick&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;45 slow frames to step down, 30 quick ones to step back up. An earlier version kept its sharpness fix to the first round, and later rounds went soft; stepping both ways fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The paddle follows the pointer
&lt;/h2&gt;

&lt;p&gt;In Claude Code's fullscreen layout (&lt;code&gt;CLAUDE_CODE_NO_FLICKER=1&lt;/code&gt;) the terminal reports every pointer move, not just clicks and drags. An invisible &lt;code&gt;Client&lt;/code&gt; lies over the picture and posts where the pointer is, and the paddle goes there:&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;if &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;x&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&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;isInside&lt;/span&gt; &lt;span class="o"&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;aim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&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;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;W&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;pad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isInside&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&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;awaySince&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move the pointer off the field to read Claude's answer and the game pauses after a moment, so the ball isn't lost while you look away. Bring it back and play goes on. The keys work too (&lt;code&gt;a&lt;/code&gt; and &lt;code&gt;d&lt;/code&gt;), but a terminal never says when a key comes up, so a held key moves in steps. The mouse is how it's meant to be played.&lt;/p&gt;

&lt;h2&gt;
  
  
  What recording the clip taught me
&lt;/h2&gt;

&lt;p&gt;The clip above shows Claude reading three files, writing a &lt;code&gt;DOCS.md&lt;/code&gt; and running the tests while the game plays. The first takes stuttered: the field froze for over half a second whenever Claude wrote its file.&lt;/p&gt;

&lt;p&gt;It wasn't the game. I recorded control takes and logged Claude Code's main-thread CPU four times a second:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;take&lt;/th&gt;
&lt;th&gt;effects&lt;/th&gt;
&lt;th&gt;Claude working&lt;/th&gt;
&lt;th&gt;longest freeze&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;567 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;733 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;233 ms (the serve pause)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both freezes landed on the second Claude Code drew its 16-line file write, with the main thread at 92–100% for about three quarters of a second. While it is drawing a big tool result, a mod can't paint. So if you build a mod with animation, expect the hitch when Claude writes a long file. And if you record one, start the game after the big writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it touches
&lt;/h2&gt;

&lt;p&gt;It reads and changes nothing the model sees. &lt;code&gt;claude plugin validate&lt;/code&gt; lists every hook and call before any of its 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=bricks}, ui.render{component=Pane}, ui.message, ui.close, session.end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No prompt, tool or attachment hook, no network call, no child process. The best score is kept in the plugin's own store.&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 brickfall@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;/bricks&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. Use kitty for the real picture; Ghostty speaks the same protocol but I haven't tried it, and every other terminal gets the blocks. It has been played in kitty on Linux, and its 36 tests pass in CI on 2.1.285 and 2.1.288. If you play it on a Mac, Windows or Ghostty, tell me how it went.&lt;/p&gt;

&lt;p&gt;The code is MIT, in &lt;code&gt;reporails/arcade&lt;/code&gt; on GitHub.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>claude</category>
      <category>gamedev</category>
    </item>
    <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>
  </channel>
</rss>
