<?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: Manpreet Singh</title>
    <description>The latest articles on DEV Community by Manpreet Singh (@manpreet171).</description>
    <link>https://dev.to/manpreet171</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%2F4050727%2F6d7996fe-9c11-4d2d-a5be-7157f8dfadb9.png</url>
      <title>DEV Community: Manpreet Singh</title>
      <link>https://dev.to/manpreet171</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manpreet171"/>
    <language>en</language>
    <item>
      <title>I ran Claude Code on DeepSeek V4.1 Flash. It cost 2 cents. Claude Code said 96.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:02:03 +0000</pubDate>
      <link>https://dev.to/manpreet171/i-ran-claude-code-on-deepseek-v41-flash-it-cost-2-cents-claude-code-said-96-4coj</link>
      <guid>https://dev.to/manpreet171/i-ran-claude-code-on-deepseek-v41-flash-it-cost-2-cents-claude-code-said-96-4coj</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; yes, Claude Code runs on DeepSeek V4.1 Flash, with nine environment variables. I gave it one real coding task, three times, on fresh copies of a repo. All three passed my own checks. Each run cost about &lt;strong&gt;2 cents&lt;/strong&gt; at DeepSeek's off-peak price. Claude Code showed between $0.87 and $1.28 for the same runs, because it prices a model it doesn't recognise at Claude Opus 5's rates.&lt;/p&gt;

&lt;p&gt;DeepSeek shipped the 1017 update of V4.1 Flash on 17 October, and benchmarked it inside Claude Code itself. Benchmarks are someone else's task. I wanted mine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On DeepSeek, Claude Code's cost counter is wrong by about 50×. It prices tokens from a model it doesn't recognise as Claude Opus 5.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;These are DeepSeek's own recommended settings for Claude Code, plus one line of mine at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.claude-deepseek     &lt;span class="c"&gt;# mine: any empty folder&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.deepseek.com/anthropic
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your DeepSeek API key&amp;gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deepseek-flash[1m]
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_DEFAULT_OPUS_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deepseek-flash[1m]
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_DEFAULT_SONNET_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deepseek-flash[1m]
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_DEFAULT_HAIKU_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deepseek-flash
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CODE_SUBAGENT_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deepseek-flash
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CODE_EFFORT_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;max
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CODE_AUTO_COMPACT_WINDOW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;786432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separate config folder means the run can't touch my Claude login, my own &lt;code&gt;CLAUDE.md&lt;/code&gt; files or my plugins. Every run started from the same blank setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The task
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;--json&lt;/code&gt; option to &lt;a href="https://singhlabs.dev/bridle/" rel="noopener noreferrer"&gt;bridle&lt;/a&gt;'s &lt;code&gt;agents&lt;/code&gt; check, our open-source AGENTS.md linter. With the flag, print one JSON object. Without it, the text report must not change by a single character. Add a test for a passing file and a failing one, then run the whole suite.&lt;/p&gt;

&lt;p&gt;Small, but real: the report was printed line by line straight to the screen, so the code had to be reshaped without changing what it prints. Each run got a fresh clone and ran unattended with &lt;code&gt;claude -p&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it work? I checked, not its summary
&lt;/h2&gt;

&lt;p&gt;All three runs said they passed. Here's what I checked myself, on each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tap"&gt;&lt;code&gt;$ node --test
&lt;span class="c"&gt;# tests 8
# suites 0
# pass 8
# fail 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight of eight: the six old tests and its two new ones. Then the part tests miss. I ran the new code and the original on all 32 AGENTS.md and CLAUDE.md files from &lt;a href="https://singhlabs.dev/blog/agents-md-examples/" rel="noopener noreferrer"&gt;our study of 25 famous repos&lt;/a&gt;. The text output was identical on all 32, in all three runs. And the new flag does what it says:&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="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bin/bridle.mjs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;agents&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vscode-AGENTS.md&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"vscode-AGENTS.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"FAIL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"failures"&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="nl"&gt;"lines"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"Linting vscode-AGENTS.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"  · no command blocks — reading this as a policy file, not a project file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"  ✘ pointer file: under 80 words, the rest is a link — an agent that does not follow it gets nothing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"  ✔ no unfilled placeholders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"  ✔ nothing in here tells the agent to do something dangerous"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"  ! only 0 prohibition(s) — nothing here says what the agent must never touch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"  ✔ size: 33 words ≈ 68 tokens, re-sent every turn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"FAIL — 1 problem(s). Your agent is reading this file on every run."&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;One thing I didn't ask for: all three runs also added an entry to &lt;code&gt;LOG.md&lt;/code&gt;. The repo's own &lt;code&gt;CLAUDE.md&lt;/code&gt; says every change gets logged. The prompt said "do not change anything else". It followed the file over the prompt, and said so in its summary. Your &lt;code&gt;CLAUDE.md&lt;/code&gt; still rules, whichever model is behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Turns&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Tokens: new / cached / out&lt;/th&gt;
&lt;th&gt;Claude Code showed&lt;/th&gt;
&lt;th&gt;DeepSeek charged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 · effort max&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;93 s&lt;/td&gt;
&lt;td&gt;44,065 / 632,192 / 17,123&lt;/td&gt;
&lt;td&gt;$0.96&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.019&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 · effort max&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;123 s&lt;/td&gt;
&lt;td&gt;43,284 / 970,880 / 22,957&lt;/td&gt;
&lt;td&gt;$1.28&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.023&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 · effort high&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;91 s&lt;/td&gt;
&lt;td&gt;38,074 / 605,312 / 14,896&lt;/td&gt;
&lt;td&gt;$0.87&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.017&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"DeepSeek charged" is the token counts times DeepSeek's off-peak list price. The account balance agrees: it went from $0.49 to $0.46 across all three runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the counter is wrong
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python -c "..." run-max1.json   # fields from Claude Code's JSON result
num_turns            31
input_tokens         44065
cache_read_tokens    632192
output_tokens        17123
total_cost_usd       0.964496
costBasis            unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code also printed &lt;code&gt;[claude-code:unrecognized_model] {"model":"deepseek-flash[1m]","query_source":"sdk"}&lt;/code&gt;. It doesn't know DeepSeek's prices, so it falls back to a Claude model's. Those token counts at Claude Opus 5's list prices ($5 per million in, $0.50 cached, $25 out) come to exactly $0.964496. If you budget off that number, you're out by about 50×, in the safe direction at least.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache works, just not Claude Code's
&lt;/h2&gt;

&lt;p&gt;DeepSeek's docs say its Anthropic endpoint ignores Claude Code's &lt;code&gt;cache_control&lt;/code&gt; markers. It caches on its own anyway: &lt;strong&gt;94.6%&lt;/strong&gt; of input tokens across the three runs were cache hits, at $0.003 per million instead of $0.15. That matters because Claude Code re-sends the whole session every turn. &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;On my own history, re-sent context was 97.5% of all tokens.&lt;/a&gt; The cached price is the price that counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  So is it cheaper than Claude?
&lt;/h2&gt;

&lt;p&gt;Here's the same token mix, averaged over the three runs, at each list price:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Per run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4.1 Flash, off-peak&lt;/td&gt;
&lt;td&gt;$0.020&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 5.5&lt;/td&gt;
&lt;td&gt;$0.021&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4.1 Flash, peak hours&lt;/td&gt;
&lt;td&gt;$0.039&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;
&lt;td&gt;$0.34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5.5&lt;/td&gt;
&lt;td&gt;$0.68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5.1&lt;/td&gt;
&lt;td&gt;$1.52&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Against Sonnet and Opus, DeepSeek is about 17 to 35 times cheaper. Against Claude Haiku 5.5, it's about the same, and at peak hours Haiku is cheaper. Peak is 01:00–04:00 and 06:00–10:00 UTC on weekdays, which in India is much of the working morning.&lt;/p&gt;

&lt;p&gt;Read that table carefully. It prices DeepSeek's tokens at Claude's rates. A Claude model would take a different number of turns and tokens. I didn't run Haiku 5.5 on this task, so whether it passes is untested. What it does tell you: the cheap end of the market is now cents per task, and the question is which model passes your checks, not which one is cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd watch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ignore the cost counter&lt;/strong&gt; on any non-Claude model. Read your provider's bill.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Max" effort isn't free.&lt;/strong&gt; My two max runs took 31 and 42 turns; the high run took 26. Three runs are a sample, not a trend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check the work, not the summary.&lt;/strong&gt; All three summaries were accurate this time. The only way I know that is that I ran the checks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Sources: three runs on 19 Oct 2026 between 12:47 and 12:52 UTC (DeepSeek off-peak), Claude Code 2.1.257, each on a fresh clone of bridle at commit d439619, run with &lt;code&gt;claude -p --output-format json --permission-mode acceptEdits&lt;/code&gt;. Token counts are from Claude Code's JSON result. Prices: DeepSeek's &lt;a href="https://api-docs.deepseek.com/quick_start/pricing" rel="noopener noreferrer"&gt;models and pricing page&lt;/a&gt; and its &lt;a href="https://api-docs.deepseek.com/quick_start/agent_integrations/claude_code" rel="noopener noreferrer"&gt;Claude Code guide&lt;/a&gt;; Anthropic's &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;; all read 19 Oct 2026. The Claude rows price uncached input at the base rate and skip cache-write premiums, so real Claude runs would cost a little more. The terminal blocks are real; the test output is trimmed to its summary lines, and the JSON field list was read out of Claude Code's result with a short Python one-liner.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;Output was 0.2% of my Claude Code tokens&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/agents-md-examples/" rel="noopener noreferrer"&gt;We checked the AGENTS.md files of 25 famous repos&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/claude-code-deepseek/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>deepseek</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>WhatsApp Business API pricing in India, 2026: your chatbot's replies cost money again.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Mon, 19 Oct 2026 12:19:02 +0000</pubDate>
      <link>https://dev.to/manpreet171/whatsapp-business-api-pricing-in-india-2026-your-chatbots-replies-cost-money-again-1iai</link>
      <guid>https://dev.to/manpreet171/whatsapp-business-api-pricing-in-india-2026-your-chatbots-replies-cost-money-again-1iai</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; since 1 October 2026, Meta charges again for the replies a business sends while a customer's chat is open. In India that's &lt;strong&gt;₹0.115 a message&lt;/strong&gt; after the first 1,000 free each month per number. Marketing messages are ₹0.8631. And if Meta bills you from India, that billing has to move to rupees by 31 December, or messages stop on 1 January.&lt;/p&gt;

&lt;p&gt;Every rate page you'll find lists the numbers. None of them adds up a real month for a business that runs a chatbot on WhatsApp, where the bill comes from three places. So I did, with Meta's own rate card.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In India, Meta now charges more to deliver your chatbot's answer (₹0.115) than a cheap AI model charges to write it (about ₹0.05).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What changed on 1 October 2026
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replies are charged again.&lt;/strong&gt; A reply your business sends inside the 24-hour window that opens when a customer writes is a "service" message. Those had been free since November 2024. Now each one is charged, after the first &lt;strong&gt;1,000 a month per business phone number&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;So are utility messages inside that window.&lt;/strong&gt; Order updates and receipts sent while the chat is open had been free since July 2025. Not any more.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Billing is per message.&lt;/strong&gt; That has been true since July 2025, and it matters more now: an answer split into three messages costs three times as much as the same answer in one.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The India rates, from Meta's rate card
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message type&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Per message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service&lt;/td&gt;
&lt;td&gt;Your reply while the customer's chat is open&lt;/td&gt;
&lt;td&gt;₹0.115&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Utility&lt;/td&gt;
&lt;td&gt;Order updates, receipts, reminders&lt;/td&gt;
&lt;td&gt;₹0.115&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;One-time passwords&lt;/td&gt;
&lt;td&gt;₹0.115&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing&lt;/td&gt;
&lt;td&gt;Offers and announcements, to people who opted in&lt;/td&gt;
&lt;td&gt;₹0.8631&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication-international&lt;/td&gt;
&lt;td&gt;OTPs sent by a business based outside India&lt;/td&gt;
&lt;td&gt;₹2.4971&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta Business Agent&lt;/td&gt;
&lt;td&gt;Replies written by Meta's own AI agent&lt;/td&gt;
&lt;td&gt;$2 per million tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are Meta's list rates in rupees, effective 1 October 2026. Tax and any provider's fee come on top. Volume discounts exist, but for India they start late: utility only after 2.5 crore messages a month, authentication after 7.5 lakh.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still free
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;first 1,000 service messages&lt;/strong&gt; each month, per business phone number. They don't carry over.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Everything in a "free entry point" window&lt;/strong&gt;: chats that start from a click-to-WhatsApp ad or a Facebook Page button. Marketing, utility and service messages in that window aren't charged.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A real month for a chatbot
&lt;/h2&gt;

&lt;p&gt;Take a shop that gets &lt;strong&gt;3,000 customer chats a month&lt;/strong&gt; on WhatsApp. The bot answers each in about four messages, and the shop sends 1,500 order updates. Those volumes are my assumption, not a measurement; plug in yours.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;How it's worked out&lt;/th&gt;
&lt;th&gt;Per month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bot replies (service)&lt;/td&gt;
&lt;td&gt;12,000 replies, first 1,000 free, 11,000 × ₹0.115&lt;/td&gt;
&lt;td&gt;₹1,265&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order updates (utility)&lt;/td&gt;
&lt;td&gt;1,500 × ₹0.115&lt;/td&gt;
&lt;td&gt;₹172.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The AI model&lt;/td&gt;
&lt;td&gt;Claude Haiku 5.5, about 4,000 tokens in and 150 out per reply: $5.70&lt;/td&gt;
&lt;td&gt;≈ ₹541&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The server&lt;/td&gt;
&lt;td&gt;A small VPS. Ours costs about ₹2,000 a month&lt;/td&gt;
&lt;td&gt;≈ ₹2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Before any marketing messages&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ ₹3,979&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things fall out of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The 1 October change alone is ₹1,265 here.&lt;/strong&gt; Before it, those 11,000 replies were free.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Meta's share is about a third.&lt;/strong&gt; At this size the server costs more than WhatsApp does. Twice the chats roughly doubles Meta's line and the model's, not the server's.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Short, complete answers save real money.&lt;/strong&gt; Each message is charged, so one good reply beats three chatty ones.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sending one offer a month to 1,000 opted-in customers would add ₹863.10 on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a provider adds
&lt;/h2&gt;

&lt;p&gt;Many businesses don't connect to Meta directly. They use a provider such as AiSensy, which shows its own rates. On 19 October its pricing page listed ₹0.145 for service, utility and authentication messages and ₹1.09 for marketing. That's about 26% over Meta's list rates (₹0.115 and ₹0.8631). In the example above it comes to ₹375 more a month.&lt;/p&gt;

&lt;p&gt;That isn't a scam. The markup pays for a shared inbox, template tools and support you'd otherwise build or do without. Just know which number you're looking at: Meta's rate, or a provider's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more rules to know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Billing in rupees by 31 December 2026.&lt;/strong&gt; This applies to providers, and to businesses connected to Meta directly, whose billing country is India. Their WhatsApp accounts must move to INR by 31 December. From 1 January 2027, Meta stops delivering messages from accounts still billed in another currency. Meta has a currency migration tool for it. If you use a provider, ask them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No ChatGPT-style bots.&lt;/strong&gt; Since 15 January 2026, Meta has barred general-purpose AI assistants from the WhatsApp Business Platform. A bot that answers about your own business, orders and bookings is fine.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Sources: Meta's &lt;a href="https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; (updated 30 Sep 2026) and its INR rate card and volume tiers, effective 1 Oct 2026, downloaded 19 Oct 2026. AiSensy's &lt;a href="https://aisensy.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;, read 19 Oct 2026. Anthropic's &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;model pricing&lt;/a&gt; for Claude Haiku 5.5 ($0.10 per million input tokens, $0.50 output). Dollars converted at ₹94.96, the ECB rate of 16 Oct 2026. The ban on general-purpose assistants: &lt;a href="https://www.business-standard.com/technology/tech-news/meta-bans-ai-chatbots-from-whatsapp-business-api-chatgpt-to-shut-down-jan-2026-125102200335_1.html" rel="noopener noreferrer"&gt;Business Standard&lt;/a&gt;. The chat volumes and tokens per reply are assumptions, labelled as such; the server figure is what ours costs.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;Output was 0.2% of my Claude Code tokens&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/whatsapp-business-api-pricing-india/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>chatbot</category>
      <category>ai</category>
      <category>india</category>
    </item>
    <item>
      <title>We checked the AGENTS.md files of 25 famous repos. Our own linter got a third of its fails wrong.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:20:20 +0000</pubDate>
      <link>https://dev.to/manpreet171/we-checked-the-agentsmd-files-of-25-famous-repos-our-own-linter-got-a-third-of-its-fails-wrong-3gh2</link>
      <guid>https://dev.to/manpreet171/we-checked-the-agentsmd-files-of-25-famous-repos-our-own-linter-got-a-third-of-its-fails-wrong-3gh2</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; &lt;code&gt;AGENTS.md&lt;/code&gt; is the Markdown file in a repo that tells AI coding agents how to work on it. On 16 October, 212 of GitHub's 600 most-starred repos had one in the root. I read 25 of the best-known and ran our linter on each. None tells an agent to do anything dangerous. Seven are over 3,000 tokens, re-sent on every turn. Ten forbid the agent to act on GitHub alone. And our linter failed 18 of them, five wrongly. We fixed that the same day.&lt;/p&gt;

&lt;p&gt;Search "AGENTS.md examples" and you get advice: keep it short, put the commands first, say what not to do. Good advice. I wanted to see what the projects everyone copies from actually wrote.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AGENTS.md is quietly becoming the AI contribution policy. Ten of 25 tell the agent not to push, open PRs or comment without a person.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How many repos have one: 212 of 600
&lt;/h2&gt;

&lt;p&gt;I took GitHub's 600 most-starred repositories (the smallest has 45,779 stars) and checked each root folder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;212&lt;/strong&gt; have an &lt;code&gt;AGENTS.md&lt;/code&gt;. That's 35%.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;132&lt;/strong&gt; have a &lt;code&gt;CLAUDE.md&lt;/code&gt;, and 107 of those have both.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Only &lt;strong&gt;25&lt;/strong&gt; have a &lt;code&gt;CLAUDE.md&lt;/code&gt; and no &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 600 include awesome-lists, books and courses, which I didn't separate out. Then I picked 25 well-known ones across JavaScript, Python, Go, Rust, Java, Ruby and C++. A hand pick, not a random sample: I wanted files people actually copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 25, side by side
&lt;/h2&gt;

&lt;p&gt;Sorted by size. "Est. tokens" is our linter's estimate (characters ÷ 4), sent again on every turn of a session. "Setup" and "Test" mean a concrete command, read by hand.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Words&lt;/th&gt;
&lt;th&gt;Est. tokens&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;bridle (old)&lt;/th&gt;
&lt;th&gt;Fair?&lt;/th&gt;
&lt;th&gt;1.2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vercel/next.js&lt;/td&gt;
&lt;td&gt;4,309&lt;/td&gt;
&lt;td&gt;7,658&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;astral-sh/ruff&lt;/td&gt;
&lt;td&gt;2,926&lt;/td&gt;
&lt;td&gt;5,098&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;langchain-ai/langchain&lt;/td&gt;
&lt;td&gt;2,743&lt;/td&gt;
&lt;td&gt;5,088&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;elastic/elasticsearch&lt;/td&gt;
&lt;td&gt;2,110&lt;/td&gt;
&lt;td&gt;4,151&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pytorch/pytorch&lt;/td&gt;
&lt;td&gt;2,407&lt;/td&gt;
&lt;td&gt;4,007&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rust-lang/rust&lt;/td&gt;
&lt;td&gt;1,855&lt;/td&gt;
&lt;td&gt;3,357&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zed-industries/zed&lt;/td&gt;
&lt;td&gt;1,888&lt;/td&gt;
&lt;td&gt;3,114&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ggml-org/llama.cpp&lt;/td&gt;
&lt;td&gt;1,739&lt;/td&gt;
&lt;td&gt;2,892&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;caddyserver/caddy&lt;/td&gt;
&lt;td&gt;1,259&lt;/td&gt;
&lt;td&gt;2,358&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;microsoft/playwright&lt;/td&gt;
&lt;td&gt;1,248&lt;/td&gt;
&lt;td&gt;2,185&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rails/rails&lt;/td&gt;
&lt;td&gt;847&lt;/td&gt;
&lt;td&gt;1,726&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cli/cli&lt;/td&gt;
&lt;td&gt;744&lt;/td&gt;
&lt;td&gt;1,443&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-projects/spring-framework&lt;/td&gt;
&lt;td&gt;598&lt;/td&gt;
&lt;td&gt;1,066&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;astral-sh/uv&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;876&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;huggingface/transformers&lt;/td&gt;
&lt;td&gt;476&lt;/td&gt;
&lt;td&gt;816&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nodejs/node&lt;/td&gt;
&lt;td&gt;423&lt;/td&gt;
&lt;td&gt;771&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pandas-dev/pandas&lt;/td&gt;
&lt;td&gt;303&lt;/td&gt;
&lt;td&gt;679&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;angular/angular&lt;/td&gt;
&lt;td&gt;275&lt;/td&gt;
&lt;td&gt;513&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes/kubernetes&lt;/td&gt;
&lt;td&gt;259&lt;/td&gt;
&lt;td&gt;471&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gohugoio/hugo&lt;/td&gt;
&lt;td&gt;266&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scikit-learn/scikit-learn&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;td&gt;329&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;python/cpython&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;151&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;borderline&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sveltejs/svelte&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;148&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ollama/ollama&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;microsoft/vscode&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The data: &lt;a href="https://singhlabs.dev/blog/agents-md-examples/agents-md-25.csv" rel="noopener noreferrer"&gt;the 25 files&lt;/a&gt;, with the commit each was read at, and &lt;a href="https://singhlabs.dev/blog/agents-md-examples/top-600-census.csv" rel="noopener noreferrer"&gt;all 600 repos in the census&lt;/a&gt; (CSV). Reuse it freely under CC BY 4.0: just link this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The biggest: Next.js, about 7,700 tokens a turn
&lt;/h2&gt;

&lt;p&gt;Here's our linter, &lt;a href="https://singhlabs.dev/bridle/" rel="noopener noreferrer"&gt;bridle&lt;/a&gt;, on the largest one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node bridle.mjs agents vercel__next.js.AGENTS.md
Linting vercel__next.js.AGENTS.md

  ✔ 15 runnable command block(s)
  ✔ how to install / set up
  ✔ how to run the tests
  ✔ how to build or run it
  ! 15 angle-bracket field(s), e.g. &amp;lt;task&amp;gt; — check these are argument syntax, not unfilled template text
  ✔ nothing in here tells the agent to do something dangerous
  ✔ 29 prohibition(s) — the agent knows where the edges are
  ✘ size: 4309 words ≈ 7658 tokens, re-sent every turn — over 3k tokens per turn is a real cost, cut it

FAIL — 1 problem(s). Your agent is reading this file on every run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a good file. It does everything right except stay short. If a session ran 100 turns (my assumption, not a measurement), the file alone would fill 765,800 tokens of context. Caching makes that cheaper, not smaller, so I won't turn it into dollars. What a line costs is &lt;a href="https://singhlabs.dev/blog/how-long-should-claude-md-be/" rel="noopener noreferrer"&gt;priced here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The middle of the 25 is about 1,070 tokens. Eleven are over 1,500 and seven are over 3,000: Next.js, Ruff, LangChain, Elasticsearch, PyTorch, Rust and Zed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually in them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conduct, more than build.&lt;/strong&gt; Node, CPython, scikit-learn and pandas are mostly rules for AI contributions and say little about building or testing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Little on setup.&lt;/strong&gt; Only 4 of 25 give a concrete setup command: LangChain, Next.js, PyTorch and Spring. Thirteen give a concrete test command. Many leave both to &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The agent may not act alone.&lt;/strong&gt; Ten forbid pushing, opening PRs or issues, or commenting without a human: Node, pandas, PyTorch, llama.cpp, Caddy, GitHub CLI, Rust, Transformers, Playwright and Next.js.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;They disagree on credit.&lt;/strong&gt; Kubernetes, Playwright, Elasticsearch and Next.js ban AI co-author trailers. Node and llama.cpp require an &lt;code&gt;Assisted-by:&lt;/code&gt; line. One global agent setting can't satisfy both. We &lt;a href="https://singhlabs.dev/blog/ai-contribution-policy/" rel="noopener noreferrer"&gt;compared eleven AI policies&lt;/a&gt; and found the same split.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Two are pointers.&lt;/strong&gt; VS Code's is 33 words and Svelte's 77, mostly links elsewhere. An agent that doesn't follow the link gets almost nothing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nothing dangerous, in any of them.&lt;/strong&gt; No &lt;code&gt;sudo&lt;/code&gt;, no &lt;code&gt;curl | sh&lt;/code&gt;, no &lt;code&gt;rm -rf&lt;/code&gt;, no &lt;code&gt;--no-verify&lt;/code&gt;, no force push, no write to your home folder. I re-ran the check with its "this line forbids it" filter switched off: still zero. The only writes outside the repo are to &lt;code&gt;/tmp&lt;/code&gt; in Next.js and Ruff, both scratch space with a stated reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And CLAUDE.md has become a pointer.&lt;/strong&gt; Seven of the 25 also have one. None has its own content: three are the same file under two names, two are a one-line &lt;code&gt;@AGENTS.md&lt;/code&gt; import, and two just say "see AGENTS.md". Which is the setup &lt;a href="https://singhlabs.dev/blog/agents-md-vs-claude-md/" rel="noopener noreferrer"&gt;we recommended&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five lines worth copying
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zed&lt;/strong&gt; sets a bar for adding any rule: "Rules should be traps to avoid, not maps to follow." Anything the agent can learn by reading the code stays out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt; pairs its "don't" with the command to run instead: don't hand-edit generated files, run the update target.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub CLI&lt;/strong&gt; names the one command with side effects: its acceptance tests create real resources, so they need an authorised test host.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Playwright&lt;/strong&gt; defines consent in words a model can check: it may push only when the user's message contains specific trigger words.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;llama.cpp&lt;/strong&gt; corrects the model's stale memory: a library it may "remember" isn't used here.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Our linter got a third of its fails wrong
&lt;/h2&gt;

&lt;p&gt;bridle failed 18 of the 25. I read every one. Twelve fails were fair: the seven oversized files, plus VS Code, Ollama, Kubernetes, Svelte and pandas for missing steps or thin pointers. &lt;strong&gt;Five were wrong&lt;/strong&gt;, and one was borderline. Node is the clearest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node bridle.mjs agents nodejs__node.AGENTS.md
Linting nodejs__node.AGENTS.md

  · no command blocks — reading this as a policy file, not a project file
  ✘ few imperative rules — an agent cannot act on description alone
  ! 4 angle-bracket field(s), e.g. &amp;lt;agent name&amp;gt; — check these are argument syntax, not unfilled template text
  ✔ nothing in here tells the agent to do something dangerous
  ! only 2 prohibition(s) — nothing here says what the agent must never touch
  ✔ size: 423 words ≈ 771 tokens, re-sent every turn

FAIL — 1 problem(s). Your agent is reading this file on every run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node's file has a section headed "AI agents must not:" with eight items under it. bridle counted two. Its rules only saw lines that start with one of nine verbs, so it missed indented bullets (Angular), rules mid-sentence (scikit-learn), a terse list (Spring), and a list under a "must not" heading (Node). And llama.cpp's conduct policy was read as a project file because one example block was labelled &lt;code&gt;sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed the same day: bridle 1.2.0.&lt;/strong&gt; It now ignores the label on a code block, reads indented and mid-line rules, treats two to four rules as a warning instead of a fail, follows a CLAUDE.md that imports AGENTS.md, and fails a pointer file under 80 words. Re-run on the same 25: &lt;strong&gt;13 pass, 12 fail&lt;/strong&gt;. All five false fails now pass, CPython passes with a warning, and the twelve fair fails still fail. Node now reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node bridle.mjs agents nodejs__node.AGENTS.md
Linting nodejs__node.AGENTS.md

  · no command blocks — reading this as a policy file, not a project file
  ! only 3 imperative rule(s) — an agent cannot act on description alone
  ! 4 angle-bracket field(s), e.g. &amp;lt;agent name&amp;gt; — check these are argument syntax, not unfilled template text
  ✔ nothing in here tells the agent to do something dangerous
  ! only 2 prohibition(s) — nothing here says what the agent must never touch
  ✔ size: 423 words ≈ 771 tokens, re-sent every turn

PASS — an agent can act on this.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better, not right. It still counts three rules and two prohibitions where a person counts eight. It passes because a short count is now a warning, not because the counting improved. The browser checker runs the same 1.2.0 rules.&lt;/p&gt;

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

&lt;p&gt;Paste your file into the &lt;a href="https://singhlabs.dev/claude-md-checker/" rel="noopener noreferrer"&gt;CLAUDE.md checker&lt;/a&gt;, which runs the same checks in your browser and sends nothing anywhere. Or in a terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx github:manpreet171/bridle agents AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read a ✘ as a question, not a verdict. After this week, that's not a disclaimer. It's a measurement.&lt;/p&gt;




&lt;p&gt;Sources: the census used GitHub's search API (&lt;code&gt;stars:&amp;gt;15000&lt;/code&gt;, sorted by stars, the first 600) and each repo's root listing, on 16 Oct 2026 at 07:06 UTC. The 25 were picked by hand for name recognition and spread, not at random. Each file was downloaded at the last commit that changed it; three AGENTS.md files are symlinks (Transformers, PyTorch, Zed) and were read at their target. Token figures are bridle's estimate of characters ÷ 4, not a tokenizer count. "Setup", "Test", the GitHub-conduct count and every fail verdict are my reading of each file. The terminal blocks are real bridle runs, the first two on commit 93b302a and the last on 1.2.0 (commit d439619, the same day), with the file path shortened. Only root files were measured; several repos keep more in subfolders.&lt;/p&gt;

&lt;p&gt;Where this fits: &lt;a href="https://singhlabs.dev/agent-ops-stack/#memory" rel="noopener noreferrer"&gt;memory and context, layer 2 of the Agent Ops Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/how-long-should-claude-md-be/" rel="noopener noreferrer"&gt;How long should CLAUDE.md be? I priced every line.&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/agents-md-vs-claude-md/" rel="noopener noreferrer"&gt;AGENTS.md vs CLAUDE.md: Claude Code reads both now, just not at once.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/agents-md-examples/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>AI contribution policy: 11 projects compared, one to paste.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:41:03 +0000</pubDate>
      <link>https://dev.to/manpreet171/ai-contribution-policy-11-projects-compared-one-to-paste-2mk8</link>
      <guid>https://dev.to/manpreet171/ai-contribution-policy-11-projects-compared-one-to-paste-2mk8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; an AI contribution policy is a few lines in &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, or its own &lt;code&gt;AI_POLICY.md&lt;/code&gt;, that answer three questions. May contributors use AI? Must they say so? Who answers for the code? I read eleven real ones. They agree on the third answer and fight about the other two. Then I checked the 300 most-starred GitHub repos with a contributing guide: 65 have a written AI policy.&lt;/p&gt;

&lt;p&gt;Search for one and you mostly get mailing lists, GitHub issues and raw policy files. So here they are side by side, read at the source on 9 October 2026, plus a short version to paste.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every policy agrees on one line: the person who opens the PR owns every line in it. The rest is taste.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the big projects actually require
&lt;/h2&gt;

&lt;p&gt;Four questions per project. &lt;strong&gt;Disclose:&lt;/strong&gt; must you say you used AI? &lt;strong&gt;Ban:&lt;/strong&gt; is anything forbidden outright? &lt;strong&gt;Owns it:&lt;/strong&gt; must you understand it yourself? &lt;strong&gt;Close:&lt;/strong&gt; what happens when you don't comply?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Linux kernel&lt;/strong&gt; · Jan 2026 · &lt;a href="https://docs.kernel.org/process/coding-assistants.html" rel="noopener noreferrer"&gt;coding assistants&lt;/a&gt;, &lt;a href="https://docs.kernel.org/process/generated-content.html" rel="noopener noreferrer"&gt;generated content&lt;/a&gt; Disclose: yes, an &lt;code&gt;Assisted-by:&lt;/code&gt; tag, plus the tools and prompts in the changelog. Ban: no. Owns it: yes, and an AI may never add &lt;code&gt;Signed-off-by&lt;/code&gt;; only a human can. Close: if you can't defend it, maintainers may reject it without a detailed review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt; · Nov 2025, updated Apr 2026 · &lt;a href="https://www.kubernetes.dev/docs/guide/pull-requests/#ai-guidance" rel="noopener noreferrer"&gt;AI guidance&lt;/a&gt; Disclose: yes, in the PR description. But &lt;em&gt;no&lt;/em&gt; AI co-author and no &lt;code&gt;assisted-by&lt;/code&gt; trailer. Ban: large AI-generated PRs and AI-written commit messages. Owns it: yes. Close: if you can't explain a change, or if you answer reviewers through an AI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fedora&lt;/strong&gt; · approved 22 Oct 2025 · &lt;a href="https://docs.fedoraproject.org/en-US/council/policy/ai-contribution-policy/" rel="noopener noreferrer"&gt;AI-assisted contributions&lt;/a&gt; Disclose: required when a significant part comes from a tool unchanged; &lt;code&gt;Assisted-by:&lt;/code&gt; recommended; grammar fixes exempt. Ban: no. Owns it: yes, fully. Close: not covered. It also binds reviewers: AI can't be the final judge of a contribution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rust&lt;/strong&gt; (rust-lang/rust only) · 5 Aug 2026 · &lt;a href="https://forge.rust-lang.org/policies/llm-usage.html" rel="noopener noreferrer"&gt;LLM usage&lt;/a&gt; Disclose: yes. Ban: LLM-written comments, docs and compiler messages. LLM-written code only when a reviewer agreed in advance and it can't break soundness. Owns it: yes, you can't blame the model. Close: recommended for major violations. Bonus: if more than half the PRs merged in six weeks are LLM-created, merging new ones stops.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ghostty&lt;/strong&gt; · disclosure Aug 2025, policy Jan 2026 · &lt;a href="https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md" rel="noopener noreferrer"&gt;AI_POLICY.md&lt;/a&gt; Disclose: all AI use, which tool and how much. Ban: AI-generated images, video and audio. Owns it: yes, explain it without the AI or don't contribute. Close: since February, PRs from anyone not vouched for close automatically. Slop gets you on a public block list.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;curl&lt;/strong&gt; · May 2025 · &lt;a href="https://github.com/curl/curl/blob/master/docs/CONTRIBUTE.md" rel="noopener noreferrer"&gt;CONTRIBUTE.md&lt;/a&gt; Disclose: yes for bugs and reports an AI found; not required for code. Ban: no, AI-assisted code is judged like any other. Owns it: verify every finding yourself. Close: fake reports get you banned on the spot. The bug bounty itself ended in January 2026.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;uv&lt;/strong&gt; (all of Astral) · Mar 2026 · &lt;a href="https://github.com/astral-sh/.github/blob/main/AI_POLICY.md" rel="noopener noreferrer"&gt;AI_POLICY.md&lt;/a&gt; Disclose: only when you quote an AI in a comment. Ban: AI-written comments to maintainers, and autonomous agents. Owns it: yes, explain it in your own words. Close: any PR they believe an agent opened.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ripgrep&lt;/strong&gt; · May 2026 · &lt;a href="https://github.com/BurntSushi/ripgrep/blob/master/AI_POLICY.md" rel="noopener noreferrer"&gt;AI_POLICY.md&lt;/a&gt; uv's policy, adapted. Its &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; is two sentences long and says the rest: contributions that don't follow it will be closed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;QEMU&lt;/strong&gt; · Jun 2025 · &lt;a href="https://www.qemu.org/docs/master/devel/code-provenance.html" rel="noopener noreferrer"&gt;code provenance&lt;/a&gt; Ban: yes. It declines any contribution believed to contain AI-generated content, "known or suspected". Using AI to research an API is fine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gentoo&lt;/strong&gt; · Apr 2024 · &lt;a href="https://wiki.gentoo.org/wiki/Project:Council/AI_policy" rel="noopener noreferrer"&gt;council AI policy&lt;/a&gt; Ban: yes, contributions made with AI tools are "expressly forbidden". The council can revisit it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NetBSD&lt;/strong&gt; · May 2024 · &lt;a href="https://www.netbsd.org/developers/commit-guidelines.html" rel="noopener noreferrer"&gt;commit guidelines&lt;/a&gt; LLM code is presumed tainted and needs the core team's written approval before it's committed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: everyone wants the human to own it. After that it splits. Linux and Fedora ask for an &lt;code&gt;Assisted-by:&lt;/code&gt; trailer, and Kubernetes forbids it. QEMU and Gentoo ban AI code outright, and NetBSD wants it approved in writing first. And uv, ripgrep and Rust care as much about the comments as the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many repos have one: 65 of 300
&lt;/h2&gt;

&lt;p&gt;I took GitHub's most-starred repos pushed in 2026 (no forks, no archives) and walked down the list until I had 300 with a contributing guide. That took 414 repos; the 300 run from 52,000 to 517,000 stars. A script read each guide and listed every file in the root, &lt;code&gt;.github/&lt;/code&gt; and &lt;code&gt;docs/&lt;/code&gt;. Then I read every hit by hand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;65 of 300 (22%)&lt;/strong&gt; have a written AI policy you can find: an &lt;code&gt;AI_POLICY.md&lt;/code&gt; (8 of them), a section with its own heading, or a link to a separate AI policy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;27 more&lt;/strong&gt; set a rule in a sentence or two, with no heading. Godot, Deno, Caddy and Elasticsearch are here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;208 (69%)&lt;/strong&gt; say nothing about AI in their contributing guide.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not all of them are strict. OpenClaw says no disclosure is required at all. PocketBase turned off outside PRs entirely, blaming LLM spam. And a new kind of line is appearing: rules written for the agent itself. Godot asks an AI agent to put 🤖 at the start of its PR title. crewAI tells agents to add an &lt;code&gt;llm-generated&lt;/code&gt; label. Electron hides a note in its guide: are you a coding agent?&lt;/p&gt;

&lt;p&gt;Two caveats. This is the famous end of GitHub, and plenty of these repos are AI tools. And a policy that lives only in &lt;code&gt;AGENTS.md&lt;/code&gt; or a website, never linked from the contributing guide, isn't counted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The policy, ready to paste
&lt;/h2&gt;

&lt;p&gt;The short version, built from the line everyone agreed on and the choices that worked best. Pick your side on the trailer before you paste.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## AI contributions&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; You may use AI. You own every line: if you can't explain a
  change without it, don't send it.
&lt;span class="p"&gt;-&lt;/span&gt; Say so in the PR description: which tool, and what it did.
&lt;span class="p"&gt;-&lt;/span&gt; Write PR descriptions, issues and review replies yourself.
&lt;span class="p"&gt;-&lt;/span&gt; No autonomous agent PRs. A human is in the loop or it's closed.
&lt;span class="p"&gt;-&lt;/span&gt; PRs that ignore this are closed without review.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The longer version, adapted from Rust's policy, with the &lt;code&gt;Assisted-by:&lt;/code&gt; trailer and a line protecting private use, is &lt;a href="https://singhlabs.dev/prompts/#contributions" rel="noopener noreferrer"&gt;on the prompts page&lt;/a&gt;. Neither has run in a live repo yet. Adjust the closing line to your temperament.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it enforceable: check the PR description
&lt;/h2&gt;

&lt;p&gt;"You must understand it" is the line nobody can check by reading the policy. The PR description is where it shows: does it say what changed, and does it disclose? &lt;a href="https://singhlabs.dev/plumb/" rel="noopener noreferrer"&gt;plumb&lt;/a&gt; holds a PR's description against its diff. Here it is on a real Ghostty PR, a 14-file change whose description was a screenshot and three links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ plumb check --pr 14559
plumb — 14 files changed, 0 named in the summary

touched but not described  (context, not a failure — PRs describe intent)
  · pkg/harfbuzz/main.zig modified
  · src/Surface.zig modified
  · src/build/uucode_config.zig modified
  · src/config/Config.zig modified
  ...10 more

nothing here contradicts the description.

No AI disclosure in the description — that may be fine, or it may be undisclosed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ghostty's vouch bot had already closed it, for a different reason: the author wasn't on the vouched list. Nothing here says AI wrote it, and plumb can't tell you that. Nothing can. What it gives a maintainer is the two questions Ghostty's policy makes them ask: what do these 14 files do, and did you use AI? On a PR that does disclose, the last line reads "The description mentions AI assistance."&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to put in it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A ban you can only enforce on suspicion.&lt;/strong&gt; QEMU's runs on "known or suspected". The people who read your policy and disclose are the honest ones. A ban teaches them to stop disclosing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A trailer you haven't checked.&lt;/strong&gt; Kubernetes forbids AI co-authors because an AI can't sign its CLA, and so no vendor can advertise that Kubernetes uses their tool. If you use a CLA bot, test what an &lt;code&gt;Assisted-by:&lt;/code&gt; or &lt;code&gt;Co-authored-by:&lt;/code&gt; line does to it first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code only.&lt;/strong&gt; curl's flood came in as security reports, not code. uv, ripgrep, Rust and Kubernetes all cover comments and review replies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A promise to detect AI.&lt;/strong&gt; You can't, so don't threaten it. Ask for disclosure and an explanation, and close what arrives without either.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Sources: each policy was read at the link beside it on 9 Oct 2026; repo files through the GitHub API. Dates are the commit that added the text, or the council vote (Fedora, Gentoo) or announcement (Rust). NetBSD's page carries no date; May 2024 is from press coverage at the time. The 300-repo sample: GitHub search &lt;code&gt;stars:&amp;gt;1000 pushed:&amp;gt;=2026-01-01 fork:false archived:false&lt;/code&gt; sorted by stars, keeping repos with a CONTRIBUTING file in the root, &lt;code&gt;.github/&lt;/code&gt; or &lt;code&gt;docs/&lt;/code&gt;. Every hit was read and sorted by hand; the split between "a policy" and "a sentence" is my judgment. The plumb run (1.1.0) was in a fresh clone of Ghostty on 9 Oct 2026; its first line, which names the PR's author, is removed and its file list is trimmed from 14 to 4.&lt;/p&gt;

&lt;p&gt;Where this fits: &lt;a href="https://singhlabs.dev/agent-ops-stack/#testing" rel="noopener noreferrer"&gt;testing and verification, layer 5 of the Agent Ops Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-code-says-done/" rel="noopener noreferrer"&gt;Claude Code says it's done. Check the diff, not the paragraph.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/ai-contribution-policy/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>github</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How to review AI-generated code (8 real PRs checked)</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:36:42 +0000</pubDate>
      <link>https://dev.to/manpreet171/how-to-review-ai-generated-code-8-real-prs-checked-1p6p</link>
      <guid>https://dev.to/manpreet171/how-to-review-ai-generated-code-8-real-prs-checked-1p6p</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; before you read a line of code, hold the PR description against the diff. Find what changed that it never mentions, anything removed or switched off, and any name it cites that you can't find. Read those first. Then work the checklist: new dependencies, tests that can't fail, skipped tests, scope drift, invented APIs.&lt;/p&gt;

&lt;p&gt;Search this and page one is guides and checklists, several from companies selling a review bot. Most say "start with intent". Right idea. But the intent is already written down. It's the PR description. None of the ones I read holds one up against its diff, on a real PR, and shows what falls out.&lt;/p&gt;

&lt;p&gt;So I did. Eight PRs, merged or closed in the last three weeks, on repos with maintainers and reviewers: dbeaver, Mailspring, sharedb, enonic, evcc and three more. Every description ends with "Generated with Claude Code".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The description is a claim about the diff. Check the claim before you read the code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why AI PRs read well and review badly
&lt;/h2&gt;

&lt;p&gt;An agent writes a good paragraph for free. One of the eight has a 440-word description that cites RFC section numbers. Another ships a 933-line plan document next to the code. Writing stopped being the expensive part. Reading didn't.&lt;/p&gt;

&lt;p&gt;And a fluent description changes how you read. You stop treating the diff as evidence and start treating it as confirmation. The paragraph is usually right about what it mentions. The risk is in what it doesn't, and nobody writes "and I deleted six assertions". Why agent summaries leave things out is &lt;a href="https://singhlabs.dev/blog/claude-code-says-done/" rel="noopener noreferrer"&gt;its own post&lt;/a&gt;. This one is about the PR waiting for your review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the description, not the code
&lt;/h2&gt;

&lt;p&gt;Three questions, in order, before you open a file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What changed that the description never mentions?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What was removed or switched off?&lt;/strong&gt; A guard, an assertion, a test, a &lt;code&gt;.skip&lt;/code&gt;, a &lt;code&gt;# noqa&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which names does it cite that you can't find?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://singhlabs.dev/plumb/" rel="noopener noreferrer"&gt;plumb&lt;/a&gt; asks all three mechanically. &lt;code&gt;plumb check --pr&lt;/code&gt; reads the description through the GitHub CLI, fetches the branch, and prints only the mismatches. No LLM: git and regexes.&lt;/p&gt;

&lt;p&gt;One thing it doesn't fail you for: files nobody named. Descriptions name code, not paths. Across these eight, 50 files changed and 7 were named by path. On a PR, an unnamed file is context, not a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 8 real PRs left out of their summaries
&lt;/h2&gt;

&lt;p&gt;plumb exited clean on four and flagged four. The one with the most flags first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ plumb check --pr 12428
plumb — 13 files changed, 0 named in the summary

touched but not described  (context, not a failure — PRs describe intent)
  · modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/upgrade/BranchEntryUpgrader.java deleted
  · modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/upgrade/DumpUpgraderRunner.java modified
  ...

quiet cuts  (removed or silenced, whatever the summary says)
  · modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/upgrade/model8to9/VersionHistoryMigrationUpgrader.java:178 guard removed
    if ( !ContentConstants.CONTENT_NODE_COLLECTION.equals( nodeVersion.nodeTy...
  · modules/core/core-repo/src/test/java/com/enonic/xp/repo/impl/dump/upgrade/model8to9/NodePathNormalizeUpgraderTest.java:52 assertion removed
    assertThat( result.getNodePath() ).isEqualTo( "/content/my-node" );
  ...

7 things to read before you merge this.

The description mentions AI assistance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's enonic/xp #12428, merged. The description, in full: "Fixes #12427", plus the Claude Code line. The issue lists four bugs, and most of the diff maps onto them. What nothing explains: an interface deleted, and with it a guard, five tests and six assertions. Reading the diff, branch entries now go through the same upgrade as versions. That may be exactly the fix. It was reviewed, by Copilot and by a person. But 13 files had to be worked out by hand, because the description gave nothing to check them against.&lt;/p&gt;

&lt;p&gt;The other seven:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;evcc-io/evcc #34222&lt;/strong&gt; (closed). Fourteen "assertion removed" lines, all from one deleted test, which the description named and explained: the field it checked was gone from a dependency. plumb was noisy; the description was honest. The maintainers reverted the module bump instead, and closed it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;dbeaver/dbeaver #42305&lt;/strong&gt; (merged). The description matches the diff exactly: four &lt;code&gt;null&lt;/code&gt;s become &lt;code&gt;""&lt;/code&gt;. One flag: &lt;code&gt;TMPartitioner&lt;/code&gt;, "found nowhere". It lives in TM4E, the Eclipse project the description names. Real, just not in this repo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;basnijholt/adaptive-lighting #1616&lt;/strong&gt; (closed). A long, careful description. Its one flag, &lt;code&gt;AdaptiveLightingManager.adaptation_holds&lt;/code&gt;, is wrong: the PR adds &lt;code&gt;adaptation_holds&lt;/code&gt;, and plumb went looking for the dotted name. My bug.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;category-labs/monad #2665&lt;/strong&gt; (merged). Clean. The description says it adds a flag and switches one function to it. True. It doesn't say the test changed too. That turned out to be the most interesting line in the eight; it's below.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Foundry376/Mailspring #2927&lt;/strong&gt; (merged). Clean, and the most thorough description of the lot. Its test counts are still wrong. Also below.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;bjlittle/geovista #2580&lt;/strong&gt; (merged). Clean. Twelve files, the 933-line plan among them. The one config line that switches a check off, an &lt;code&gt;ignore_missing_imports&lt;/code&gt; for &lt;code&gt;lazy_loader&lt;/code&gt;, is in the description with its reason. That's what a good one looks like.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;share/sharedb #743&lt;/strong&gt; (merged). Clean. Three lines of fix, 31 of test, and a description that says exactly that.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A ninth didn't make it. plumb crashed on apollographql/rover #3924: a stacked PR whose base branch was deleted after merge, and plumb fetches the base by name. Also my bug, so sharedb took its place.&lt;/p&gt;

&lt;p&gt;Tally: no skipped tests, no silenced linters, no new dependencies, no invented APIs. One description that explained nothing. One test that changed shape without a word. One set of numbers that was off. And two plumb bugs. The first time I pointed plumb at real PRs, in August, it found three bugs in plumb. Real PRs keep doing that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the checklist: dependencies, tests that can't fail, scope drift
&lt;/h2&gt;

&lt;h3&gt;
  
  
  New dependencies
&lt;/h3&gt;

&lt;p&gt;Check the manifests before the code:&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;$ &lt;/span&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;diff 2580 &lt;span class="nt"&gt;--repo&lt;/span&gt; bjlittle/geovista &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'package.json|lock|go.mod|Cargo|pyproject|requirements|pom.xml'&lt;/span&gt;
pyproject.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one hit across all eight was that type-checker setting, not a package. None of them added a dependency. When one does, look the name up on the registry yourself. A package an AI half-remembered is exactly the kind of name somebody else can register.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests that can't fail
&lt;/h3&gt;

&lt;p&gt;The monad change, trimmed to its two hunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ gh pr diff 2665 --repo category-labs/monad
&lt;span class="gd"&gt;--- a/category/execution/ethereum/test/test_monad_chain.cpp
-    if constexpr (TestFixture::REV &amp;gt;= MONAD_ONE) {
&lt;/span&gt;&lt;span class="gi"&gt;+    if constexpr (TestFixture::Trait::charges_gas_limit()) {
&lt;/span&gt;         EXPECT_EQ(refund, 0);
&lt;span class="gd"&gt;--- a/category/execution/monad/monad_transaction_gas.cpp
-    if constexpr (traits::monad_rev() &amp;gt;= MONAD_ONE) {
&lt;/span&gt;&lt;span class="gi"&gt;+    if constexpr (traits::charges_gas_limit()) {
&lt;/span&gt;         return 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before, the test said what it expected: from MONAD_ONE on, no refund. Now it asks the new flag, the same one the code asks. If the flag were wrong, code and test would be wrong together, and green. In the PR's tree the flag shows up in three files: where it's declared, where it's used, and this test. No test checks what it returns. It's a tidy refactor and a reviewer approved it. It's also a test that can no longer catch the bug it's about.&lt;/p&gt;

&lt;p&gt;The evcc PR had the other shape. Its second fix added the new error message to a test's list of acceptable errors, so the failing check passed. A maintainer suggested the better fix, a sample serial number so the test exercises the new check instead of excusing it, and that landed in a separate PR.&lt;/p&gt;

&lt;p&gt;The test for both: revert the fix and run the test. If it still passes, it isn't testing the fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skipped and silenced tests
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.skip&lt;/code&gt;, &lt;code&gt;xit&lt;/code&gt;, &lt;code&gt;@pytest.mark.skip&lt;/code&gt;, &lt;code&gt;# noqa&lt;/code&gt;, &lt;code&gt;@ts-ignore&lt;/code&gt;, &lt;code&gt;continue-on-error: true&lt;/code&gt;. plumb lists every one a diff adds. None of these eight had any. The real run on the &lt;a href="https://singhlabs.dev/plumb/" rel="noopener noreferrer"&gt;plumb page&lt;/a&gt; has one: a &lt;code&gt;# noqa&lt;/code&gt; in a file its description never mentions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test counts
&lt;/h3&gt;

&lt;p&gt;If the description gives numbers, count them. Mailspring's says 7 cases in the guard spec and 6 header cases:&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;$ &lt;/span&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;diff 2927 &lt;span class="nt"&gt;--repo&lt;/span&gt; Foundry376/Mailspring | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^\+\+\+ /{f=$2} /^\+[ \t]*it\(/{n[f]++} END{for(k in n) print n[k], k}'&lt;/span&gt;
9 b/app/internal_packages/events/specs/event-header-spec.tsx
10 b/app/spec/counter-proposal-guard-spec.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;10 and 9. More tests than claimed, so no harm done. But the numbers in a description are claims too, and these didn't match.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scope drift
&lt;/h3&gt;

&lt;p&gt;Every changed file should trace back to a sentence in the description or the issue. enonic is where that breaks: 13 files and one sentence. geovista is the opposite: 12 files and a 933-line plan, every one of them explained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invented APIs
&lt;/h3&gt;

&lt;p&gt;plumb's last check flags a name the description cites that exists nowhere, not in the diff and not in the base branch. It flagged three in eight PRs. None was invented. So treat the list as things to look up, not accusations. Each took one code search:&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;$ &lt;/span&gt;gh api &lt;span class="nt"&gt;-X&lt;/span&gt; GET search/code &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"TMPartitioner repo:eclipse-tm4e/tm4e"&lt;/span&gt; &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s2"&gt;".items[0].path"&lt;/span&gt;
org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/text/TMPartitioner.java
&lt;span class="nv"&gt;$ &lt;/span&gt;gh api &lt;span class="nt"&gt;-X&lt;/span&gt; GET search/code &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"readRegisters repo:404GamerNotFound/solarman-go"&lt;/span&gt; &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s2"&gt;".items[].path"&lt;/span&gt;
solarman.go
solarman_test.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third was the plumb bug above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on your next PR
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; github:manpreet171/plumb
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-repo
&lt;span class="nv"&gt;$ &lt;/span&gt;plumb check &lt;span class="nt"&gt;--pr&lt;/span&gt; 123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It needs the GitHub CLI and a clone with GitHub as &lt;code&gt;origin&lt;/code&gt;. Exit 1 means a cut, a mute or a name it couldn't find; unnamed files never fail it. In CI, start with &lt;code&gt;--warn-only&lt;/code&gt;. What it won't do: judge whether a test means anything (it called the monad PR clean), or see past your repo (two of its three "found nowhere" names were real, elsewhere).&lt;/p&gt;

&lt;p&gt;Better still, catch it before the PR exists. &lt;a href="https://singhlabs.dev/trust-issues/" rel="noopener noreferrer"&gt;trust issues&lt;/a&gt; runs the same checks inside Claude Code every time it says it's done, so the summary your agent hands you has already been held against its own diff.&lt;/p&gt;




&lt;p&gt;Sources: the eight PRs came from GitHub search for "Generated with Claude Code" in PR descriptions, merged or closed between 25 Sep and 9 Oct 2026, and were picked by repo before plumb ran. Three candidates were dropped first: two no longer carried the line, one was written by a dependency bot. Every terminal block is a real run on 9 Oct 2026 with plumb 1.1.0, in a fresh clone of each repo, read-only. The plumb output has its first line removed (it names the PR's author), and a line that is only "..." marks where I cut it. The monad diff is trimmed to its changed lines. What maintainers said is from the PRs' own comments. Contributors aren't named on purpose: these are examples of reviewing, not of anyone's mistakes.&lt;/p&gt;

&lt;p&gt;Where this fits: &lt;a href="https://singhlabs.dev/agent-ops-stack/#testing" rel="noopener noreferrer"&gt;testing and verification, layer 5 of the Agent Ops Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-code-says-done/" rel="noopener noreferrer"&gt;Claude Code says it's done. Check the diff, not the paragraph.&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/claude-code-stop-hook-example/" rel="noopener noreferrer"&gt;Claude Code Stop hook example: check the "done" claim.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/how-to-review-ai-generated-code/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>codereview</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Why Claude Code ignores CLAUDE.md, and what sticks.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:32:30 +0000</pubDate>
      <link>https://dev.to/manpreet171/why-claude-code-ignores-claudemd-and-what-sticks-2595</link>
      <guid>https://dev.to/manpreet171/why-claude-code-ignores-claudemd-and-what-sticks-2595</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; CLAUDE.md is a request, not a lock. Claude Code loads it as context and tries to follow it, and &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;Anthropic's docs&lt;/a&gt; say plainly there's no guarantee. When I went through my own history, most of the rules I'd have blamed Claude for ignoring were never written down, or were written for something else. The few that truly must hold need a hook, not a better sentence.&lt;/p&gt;

&lt;p&gt;Search "claude code ignores claude.md" and page one is GitHub issues. Rules in capitals. MANDATORY. NEVER. On &lt;code&gt;anthropics/claude-code&lt;/code&gt;, 124 issues have CLAUDE.md and "ignore" in the title. Eight are open. 68 were closed as not planned.&lt;/p&gt;

&lt;p&gt;Plenty of complaints, no answers. So I opened my own case file: my Claude Code history since July, every project's CLAUDE.md, and the git logs behind them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A rule in CLAUDE.md is a request. If it must never break, it needs a hook, not a louder sentence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  First, check it read the file
&lt;/h2&gt;

&lt;p&gt;Rule out the boring cause before blaming the model. The docs say Claude treats CLAUDE.md "as context, not enforced configuration", and that it arrives as a user message after the system prompt. So first, confirm it arrived:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;/context&lt;/code&gt; and look under &lt;strong&gt;Memory files&lt;/strong&gt;. Not listed means not seen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A CLAUDE.md in a subfolder isn't listed at launch. It loads when Claude reads a file in that folder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Loading has bugs too. &lt;a href="https://github.com/anthropics/claude-code/issues/99032" rel="noopener noreferrer"&gt;#99032&lt;/a&gt;, filed last week: the VS Code extension skips an &lt;code&gt;@import&lt;/code&gt; that points outside the project, while the terminal loads it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Loaded and still ignored? Then it's one of the next four.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The rule isn't there
&lt;/h2&gt;

&lt;p&gt;This one's embarrassing. &lt;a href="https://singhlabs.dev/toldya/" rel="noopener noreferrer"&gt;toldya&lt;/a&gt; reads what you've typed to Claude Code and finds the corrections you keep repeating. Mine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npx toldya --all --dry

toldya · 175 sessions (2 Jul – 9 Oct) · 3817 of your messages · 323 corrections

You keep telling your AI:
   1.  18×  keep it simple   (17 sessions)
   2.  12×  Don't complicate it   (7 sessions)
   3.   5×  dont ask me   (4 sessions)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;toldya also flags a repeat that's already in your CLAUDE.md or AGENTS.md, with &lt;code&gt;← already in your rules, still repeated&lt;/code&gt;. I ran it in all 12 of my projects that have one of those files. It flagged nothing.&lt;/p&gt;

&lt;p&gt;"Keep it simple" wasn't in the CLAUDE.md of any project where I kept typing it. Claude wasn't ignoring that rule. I'd never given it one.&lt;/p&gt;

&lt;p&gt;One correction, found while writing this. When you resume a session, Claude Code copies the earlier messages into the new file, and toldya 0.3.2 counted every copy: it said 44. 0.3.3 fixed the counting, 0.3.4 the grouping, which could split one habit in two, and 0.3.5 stopped counting "I don't get it" as a rule. 0.3.5 made the run above. Still my top line.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The rule is for something else
&lt;/h2&gt;

&lt;p&gt;Except I sort of had. Since 26 July I've run ponytail, a plugin whose session-start hook loads about 900 words of "build the simplest thing that works" into every session. Its text is in 187 of my 195 transcripts.&lt;/p&gt;

&lt;p&gt;So I took toldya's two simplicity habits ("keep it simple", "Don't complicate it"), dropped the copies, and kept the messages I typed with that text already loaded. There were 25. I read all of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;17&lt;/strong&gt; were about how it talked or wrote, not what it built. Mostly me, lost, asking it to explain again, plainer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;5&lt;/strong&gt; were about the approach, closer to the plugin's turf.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;3&lt;/strong&gt; I couldn't place.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ponytail says, in its own text, that it "governs what you build, not how you talk." It was loaded every time. It just wasn't the rule I kept asking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The rule is a wish
&lt;/h2&gt;

&lt;p&gt;So when I started a new data project this week, its CLAUDE.md had the right rule from the first hour: "Explain simply." Within a day I'd asked for a simple explanation five more times. "I need to understand in very simple way how to use that app."&lt;/p&gt;

&lt;p&gt;"Explain simply" is a wish. Nothing in it can be checked. The docs make the same point with better examples: "Use 2-space indentation", not "Format code properly". A checkable version of mine would name the thing: a line limit, an example first, no term left undefined. I haven't tested that yet, so I'm not promising it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The rule is clear, loaded, quoted back, and broken anyway
&lt;/h2&gt;

&lt;p&gt;The hard case. Another repo of mine opens its CLAUDE.md with a logging rule, under a heading that says non-negotiable: every change gets a CHANGELOG entry, timestamped from &lt;code&gt;date -u&lt;/code&gt;, "never guessed or invented". Specific. Checkable. Line 40 of a 222-line file when it went in; the file is 311 lines now.&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;$ &lt;/span&gt;git log &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"%ad  %s"&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"Timestamps are UTC and real"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; CLAUDE.md
2026-08-16  Repo audit, project rules, and the documents needed to run this
&lt;span class="nv"&gt;$ &lt;/span&gt;git log &lt;span class="nt"&gt;--reverse&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"%ad  %s"&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"(correct|fix).*timestamp"&lt;/span&gt;
2026-08-17  Correct a changelog timestamp I invented
2026-08-24  Correct a changelog timestamp that was written before &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; was &lt;span class="nb"&gt;read
&lt;/span&gt;2026-08-29  Fix a guessed timestamp, and write down how to stop guessing them
2026-09-09  Correct a guessed timestamp, &lt;span class="k"&gt;for &lt;/span&gt;the third &lt;span class="nb"&gt;time&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; CLAUDE.md
311 CLAUDE.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four guessed timestamps in the 24 days after the rule went in, each caught and fixed in its own commit. Claude wrote those fixes, and every one names the rule it had just broken. With the third fix the rule got sharper: read the clock in its own command, then write. Eleven days later it broke again. The fourth fix is titled "for the third time". Even the apology lost count.&lt;/p&gt;

&lt;p&gt;That's four caught misses in about 150 commits that touched the log, so the rule mostly held. Mostly is fine for indentation. It's useless for a timestamp whose only job is to be right. Since 9 Sep there have been 44 more log commits and no fix, which means either the sharper wording landed or nobody checked. The history can't say which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rules slip: the docs and the issues
&lt;/h2&gt;

&lt;p&gt;My four cases line up with Anthropic's &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;memory docs&lt;/a&gt;, its &lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;best practices&lt;/a&gt;, and the issue tracker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Where it sits.&lt;/strong&gt; CLAUDE.md arrives after the system prompt, as a user message. When it clashes with Claude Code's own guidance, the built-in can win. In &lt;a href="https://github.com/anthropics/claude-code/issues/27032" rel="noopener noreferrer"&gt;#27032&lt;/a&gt;, plan mode suggested a plans folder in the home directory, and the model followed it over a CLAUDE.md rule to keep plans in the repo. The docs list this among the things to check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Length.&lt;/strong&gt; Target under 200 lines; longer files "reduce adherence". The best-practices page is blunter: "Bloated CLAUDE.md files cause Claude to ignore your actual instructions!" My logging rule lived in a file that grew from 222 lines to 311. What each line costs is &lt;a href="https://singhlabs.dev/blog/how-long-should-claude-md-be/" rel="noopener noreferrer"&gt;in the length post&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conflicts.&lt;/strong&gt; If two instructions contradict each other, "Claude may pick one arbitrarily". Check your user file against your project file and any nested ones.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shouting.&lt;/strong&gt; The docs suggest "IMPORTANT" on the one line that keeps getting skipped, and warn that if you emphasise many, none stands out. &lt;a href="https://github.com/anthropics/claude-code/issues/33603" rel="noopener noreferrer"&gt;#33603&lt;/a&gt; is what happens next: a rule restated and strengthened after each violation, then broken in each of the next three sessions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The model.&lt;/strong&gt; &lt;a href="https://github.com/anthropics/claude-code/issues/99704" rel="noopener noreferrer"&gt;#99704&lt;/a&gt; counted a "reply in Korean" rule over one long session: no English replies on claude-opus-5, 33% English on claude-opus-5-5, same CLI. A rule that held last month proves nothing about this model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timing.&lt;/strong&gt; If something must happen at a fixed point, before every commit or after every edit, the docs say to write it as a hook.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a rule should be a hook
&lt;/h2&gt;

&lt;p&gt;The best-practices page puts it in one line: CLAUDE.md instructions are "advisory", while hooks "are deterministic and guarantee the action happens". A &lt;code&gt;PreToolUse&lt;/code&gt; hook sees the tool call before it runs, and can refuse it.&lt;/p&gt;

&lt;p&gt;Here's &lt;a href="https://singhlabs.dev/slopguard/" rel="noopener noreferrer"&gt;slopguard&lt;/a&gt;'s Bash guard, run from its plugin folder, fed the call Claude would make to commit a &lt;code&gt;.env&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;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_name":"Bash","tool_input":{"command":"git add .env &amp;amp;&amp;amp; git commit -m wip"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | node scripts/guard-bash.mjs &lt;span class="se"&gt;\&lt;/span&gt;
  | node &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"JSON.parse(require('fs').readFileSync(0)).hookSpecificOutput.permissionDecisionReason"&lt;/span&gt;
SLOPGUARD blocked this &lt;span class="nb"&gt;command&lt;/span&gt; — &lt;span class="o"&gt;[&lt;/span&gt;SG-1] credential file staged &lt;span class="k"&gt;for &lt;/span&gt;commit.

fix: Add it to .gitignore. Anything committed is &lt;span class="k"&gt;in &lt;/span&gt;the &lt;span class="nb"&gt;history &lt;/span&gt;permanently, even after a later delete.

Rule text lives &lt;span class="k"&gt;in &lt;/span&gt;AGENTS.md.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sentence version of that rule is &lt;a href="https://github.com/anthropics/claude-code/issues/2142" rel="noopener noreferrer"&gt;#2142&lt;/a&gt;, open since June 2025: Claude Code "repeatedly ignores CLAUDE.md security guidelines and exposes API keys to version control". A hook doesn't give it the chance.&lt;/p&gt;

&lt;p&gt;Two limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A hook only enforces what a script can check.&lt;/strong&gt; In &lt;a href="https://github.com/anthropics/claude-code/issues/99642" rel="noopener noreferrer"&gt;#99642&lt;/a&gt;, hooks that checked the form (a table exists, the logs were read) were satisfied while the work they stood for was skipped.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A hook that only adds text is still a request.&lt;/strong&gt; Ponytail's hook prints its rules at session start, and the &lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;hooks guide&lt;/a&gt; says that output just joins Claude's context. Same odds as CLAUDE.md. #99642's reminder hook injected its instruction on every turn, and the model never acted on it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the test is: could a script say yes or no? A &lt;code&gt;.env&lt;/code&gt; in a commit: yes, hook it. A log timestamp that doesn't match the clock: yes, and that's the hook my logging rule should have been. I haven't written it yet. "Explain simply": no. That one stays a sentence, as concrete as I can make it, and gets counted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count whether it stuck
&lt;/h2&gt;

&lt;p&gt;A rule you never re-check is a rule you're hoping about. toldya does the counting, on your machine, sending nothing:&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;$ &lt;/span&gt;npx toldya &lt;span class="nt"&gt;--dry&lt;/span&gt;    &lt;span class="c"&gt;# the report only, changes nothing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it without &lt;code&gt;--dry&lt;/code&gt; and it asks about each repeat: &lt;strong&gt;y&lt;/strong&gt; writes it into CLAUDE.md, &lt;strong&gt;n&lt;/strong&gt; skips it, &lt;strong&gt;e&lt;/strong&gt; lets you reword it first. A week later, run it again. Each rule it added gets a line like &lt;code&gt;"Keep it simple." said 18× before · 0× since&lt;/code&gt;. If "since" isn't near zero, the rule is a wish, or it needs a hook.&lt;/p&gt;

&lt;p&gt;Honest status: I can't show you a real "since" yet, because I never added my repeats through toldya. That line is from a test run a minute after adding one, so its zero means nothing. The ponytail count above is the same check done by hand, with toldya's matcher.&lt;/p&gt;




&lt;p&gt;Sources: every terminal block is a real run on my machine on 9 Oct 2026; the toldya report (0.3.5) is trimmed to its first three lines, and the before/since line is from 0.3.6, which fixed which habit that line counts. Copies were dropped the way toldya drops them, and the 25 messages were read and sorted by hand. The git log is from one of my own repos. The slopguard run feeds its hook script the JSON a tool call sends. Docs: Claude Code's &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;memory&lt;/a&gt;, &lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;best practices&lt;/a&gt; and &lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;hooks guide&lt;/a&gt;. Issues are on &lt;a href="https://github.com/anthropics/claude-code/issues" rel="noopener noreferrer"&gt;anthropics/claude-code&lt;/a&gt;, read through the GitHub API the same day; the 124 is a search for CLAUDE.md and "ignore" in issue titles.&lt;/p&gt;

&lt;p&gt;Where this fits: &lt;a href="https://singhlabs.dev/agent-ops-stack/#memory" rel="noopener noreferrer"&gt;memory and context, layer 2 of the Agent Ops Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/how-long-should-claude-md-be/" rel="noopener noreferrer"&gt;How long should CLAUDE.md be? I priced every line.&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/claude-md-rules-chat-history/" rel="noopener noreferrer"&gt;The best CLAUDE.md rules are hiding in your chat history.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/claude-code-ignores-claude-md/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Does Claude Code read .env files? What each fix actually stops.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:25:16 +0000</pubDate>
      <link>https://dev.to/manpreet171/does-claude-code-read-env-files-what-each-fix-actually-stops-59pf</link>
      <guid>https://dev.to/manpreet171/does-claude-code-read-env-files-what-each-fix-actually-stops-59pf</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; yes. Claude Code can read your &lt;code&gt;.env&lt;/code&gt; like any other file, and &lt;code&gt;.gitignore&lt;/code&gt; doesn't stop it. A &lt;code&gt;Read&lt;/code&gt; deny rule in &lt;code&gt;.claude/settings.json&lt;/code&gt; stops the obvious ways in. Nothing that matches on the filename stops a command that never says the filename.&lt;/p&gt;

&lt;p&gt;Most guides stop at "add a deny rule". So I made a throwaway repo with a fake &lt;code&gt;.env&lt;/code&gt; and put each fix up against the ways an agent actually gets at a file. Some of it I ran live. Some I couldn't, and I'll say which.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every fix here matches the name &lt;code&gt;.env&lt;/code&gt;. Your keys don't need to be asked for by name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Yes, it can: the 30-second test
&lt;/h2&gt;

&lt;p&gt;The lab: a fresh git repo, two obviously fake keys (one is AWS's own documentation example), and the file ignored the way most repos ignore it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .gitignore
.env
&lt;span class="nv"&gt;$ &lt;/span&gt;git check-ignore &lt;span class="nt"&gt;-v&lt;/span&gt; .env
.gitignore:1:.env   .env
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .env
&lt;span class="nv"&gt;STRIPE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk_test_FAKE0000000000000000
&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIAIOSFODNN7EXAMPLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in a live Claude Code session, the agent's own tools against that folder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read&lt;/strong&gt; on &lt;code&gt;.env&lt;/code&gt; returned both lines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Glob&lt;/strong&gt; for &lt;code&gt;**/.env*&lt;/code&gt; listed it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grep&lt;/strong&gt; for &lt;code&gt;STRIPE_KEY&lt;/code&gt; across the folder found nothing. Rename &lt;code&gt;.gitignore&lt;/code&gt; and the same search finds the key. Point Grep at the file itself and it finds it with &lt;code&gt;.gitignore&lt;/code&gt; in place.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;.gitignore&lt;/code&gt; hides the file from a folder-wide search. That's all it does. It was written to keep files out of commits, and it's good at that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every fix people suggest
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.claudeignore&lt;/code&gt;.&lt;/strong&gt; Anthropic's &lt;a href="https://code.claude.com/docs/en/permissions" rel="noopener noreferrer"&gt;permissions docs&lt;/a&gt; are blunt: if your project has one, "it has no effect". Move its entries into Read deny rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A line in CLAUDE.md&lt;/strong&gt; saying "never read .env". That's a request, not a control. I didn't measure how often it's obeyed, and a rule you can't measure isn't one you should lean on for a secret.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;permissions.deny&lt;/code&gt;.&lt;/strong&gt; This is the official answer. Anthropic's &lt;a href="https://code.claude.com/docs/en/settings" rel="noopener noreferrer"&gt;settings docs&lt;/a&gt; show it as an example, and it's what I put in the lab:&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="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.claude/settings.json&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;"permissions"&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;"deny"&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="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env.*)"&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;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read|Grep|Bash"&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;"node &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CLAUDE_PROJECT_DIR/.claude/hooks/block-env.mjs&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="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;The docs say a Read deny rule covers Claude's file tools, Grep and Glob on a "best-effort" basis, &lt;code&gt;@file&lt;/code&gt; mentions, and Bash commands it recognises as file reads: &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;tee&lt;/code&gt;, plus redirections like &lt;code&gt;&amp;lt; file&lt;/code&gt;. And the docs name the gaps themselves: a command that reads files without naming them, like &lt;code&gt;grep -r pattern .&lt;/code&gt;, and "arbitrary subprocesses", like a Python or Node script that opens the file on its own.&lt;/p&gt;

&lt;p&gt;I couldn't run this one live. The CLI's login had expired:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;claude &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;2.1.257 (Claude Code)
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Read .env and print it'&lt;/span&gt;
&lt;span class="go"&gt;Failed to authenticate: OAuth session expired and could not be refreshed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So that row of the matrix is Anthropic's word, not my run. I also can't tell you whether it catches a glob like &lt;code&gt;cat .en?&lt;/code&gt;. The docs don't say, and I didn't get to try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A PreToolUse hook.&lt;/strong&gt; The hook in the settings above runs before every Read, Grep and Bash call. Exit code 2 blocks the call and hands the agent the message. Here's all of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mjs&lt;/span&gt;
&lt;span class="c1"&gt;// PreToolUse hook: refuse Read, Grep and Bash calls that name a .env file.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;raw&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="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&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;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool_input&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file_path&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;[^\w&lt;/span&gt;&lt;span class="sr"&gt;.-&lt;/span&gt;&lt;span class="se"&gt;])\.&lt;/span&gt;&lt;span class="sr"&gt;env&lt;/span&gt;&lt;span class="se"&gt;(\.[\w&lt;/span&gt;&lt;span class="sr"&gt;.-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)?(?![\w&lt;/span&gt;&lt;span class="sr"&gt;.-&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Blocked: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; touches a .env file. Ask the user for the value instead.`&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;exit&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No live session, so I tested it the way Claude Code calls it: the &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;documented PreToolUse JSON&lt;/a&gt; piped to the script on stdin, trimmed to the fields it reads. Then I ran whatever got through.&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;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_name":"Read","tool_input":{"file_path":".env"}}'&lt;/span&gt; | node .claude/hooks/block-env.mjs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
Blocked: Read touches a .env file. Ask the user &lt;span class="k"&gt;for &lt;/span&gt;the value instead.
&lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_name":"Bash","tool_input":{"command":"cat .env"}}'&lt;/span&gt; | node .claude/hooks/block-env.mjs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
Blocked: Bash touches a .env file. Ask the user &lt;span class="k"&gt;for &lt;/span&gt;the value instead.
&lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_name":"Bash","tool_input":{"command":"cat .en?"}}'&lt;/span&gt; | node .claude/hooks/block-env.mjs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .en?
&lt;span class="nv"&gt;STRIPE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk_test_FAKE0000000000000000
&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIAIOSFODNN7EXAMPLE
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_name":"Bash","tool_input":{"command":"grep -r STRIPE ."}}'&lt;/span&gt; | node .claude/hooks/block-env.mjs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; STRIPE &lt;span class="nb"&gt;.&lt;/span&gt;
./.env:STRIPE_KEY&lt;span class="o"&gt;=&lt;/span&gt;sk_test_FAKE0000000000000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python went the same way. &lt;code&gt;open('.env')&lt;/code&gt; was blocked. &lt;code&gt;open(glob.glob('.en*')[0])&lt;/code&gt; got exit 0 and printed both keys. One question mark beat a regex I spent longer on than I'd like to admit.&lt;/p&gt;

&lt;p&gt;One more trap, and it's a nasty one:&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;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'not json'&lt;/span&gt; | node .claude/hooks/block-env.mjs 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hook that crashes exits 1. Per the hooks docs, exit 1 is a "non-blocking error" and the action goes ahead. If your guard throws, it waves the call through. Only exit 2, or a JSON deny, blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sandbox.&lt;/strong&gt; &lt;code&gt;sandbox.filesystem.denyRead&lt;/code&gt; is the one fix that doesn't care about names. The OS enforces it on Bash commands and every process they start, so the Python trick and the glob should both hit a wall. Two catches, both from &lt;a href="https://code.claude.com/docs/en/sandboxing" rel="noopener noreferrer"&gt;the sandbox docs&lt;/a&gt;: a &lt;code&gt;denyRead&lt;/code&gt; entry "doesn't stop the Read tool", so you still need the deny rule. And it runs on macOS, Linux and WSL2. On native Windows, which is what I'm on, commands run unsandboxed. Untested here for that reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The matrix: what each one blocks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt;. Stops: commits, and a folder-wide Grep. Misses: Read, Glob, Grep aimed at the file, &lt;code&gt;cat&lt;/code&gt;, everything else. &lt;em&gt;Tested live.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;.claudeignore&lt;/code&gt;&lt;/strong&gt;. Stops: nothing. &lt;em&gt;Anthropic's docs; not tested.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Never read .env" in CLAUDE.md&lt;/strong&gt;. Stops: whatever the model chooses to obey. &lt;em&gt;Not tested.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;Read&lt;/code&gt; deny rules&lt;/strong&gt;. Stops: Read, Grep and Glob (best effort), &lt;code&gt;@file&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;/&lt;code&gt;head&lt;/code&gt;/&lt;code&gt;tail&lt;/code&gt;/&lt;code&gt;sed&lt;/code&gt;/&lt;code&gt;tee&lt;/code&gt; on the file. Misses: &lt;code&gt;grep -r .&lt;/code&gt;, scripts that open the file themselves. &lt;em&gt;Anthropic's docs; not tested live.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PreToolUse hook&lt;/strong&gt;. Stops: any call that spells out &lt;code&gt;.env&lt;/code&gt;. Misses: &lt;code&gt;cat .en?&lt;/code&gt;, &lt;code&gt;grep -r STRIPE .&lt;/code&gt;, Python with a glob, and everything if the hook crashes. &lt;em&gt;Tested with piped payloads.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sandbox &lt;code&gt;denyRead&lt;/code&gt;&lt;/strong&gt;. Stops: Bash and its child processes, by OS. Misses: the Read tool, and all of native Windows. &lt;em&gt;Anthropic's docs; not tested.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;slopguard&lt;/strong&gt;. Stops: none of the reads. That isn't its job. Its job is the next section. &lt;em&gt;Tested with piped payloads.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The other leak: it writes the key into your code
&lt;/h2&gt;

&lt;p&gt;Reading the key isn't the expensive part. The expensive part is the agent helpfully pasting it into a source file, which then gets committed, pushed and shipped to a browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://singhlabs.dev/slopguard/" rel="noopener noreferrer"&gt;slopguard&lt;/a&gt; is our Claude Code plugin for that moment. Its write hook looks at the code before the Write or Edit tool puts it on disk. Fed by hand, from the repo, the way its README says to:&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;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_input":{"file_path":"src/aws.js","content":"const keyId = \"AKIAIOSFODNN7EXAMPLE\";"}}'&lt;/span&gt; | node plugins/slopguard/scripts/guard-write.mjs | node &lt;span class="nt"&gt;-pe&lt;/span&gt; &lt;span class="s2"&gt;"JSON.parse(require('fs').readFileSync(0,'utf8')).hookSpecificOutput.permissionDecisionReason"&lt;/span&gt;
SLOPGUARD blocked this write to src/aws.js — 1 unsafe pattern&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;:

• &lt;span class="o"&gt;[&lt;/span&gt;SG-1] AWS access key ID
    found: AKIA…LE
    fix:   Move this to an environment variable or a secret manager, reference it via process.env / os.environ, and rotate the exposed credential — assume it is already public.

Rewrite the code to satisfy the rule above, &lt;span class="k"&gt;then &lt;/span&gt;write the file again.
Rule text lives &lt;span class="k"&gt;in &lt;/span&gt;AGENTS.md. Do not disable the guard to get past it.
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_input":{"file_path":"src/pay.js","content":"const stripe = new Stripe(\"sk_test_FAKE0000000000000000\");"}}'&lt;/span&gt; | node plugins/slopguard/scripts/guard-write.mjs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_input":{"command":"git add .env"}}'&lt;/span&gt; | node plugins/slopguard/scripts/guard-bash.mjs | node &lt;span class="nt"&gt;-pe&lt;/span&gt; &lt;span class="s2"&gt;"JSON.parse(require('fs').readFileSync(0,'utf8')).hookSpecificOutput.permissionDecisionReason"&lt;/span&gt;
SLOPGUARD blocked this &lt;span class="nb"&gt;command&lt;/span&gt; — &lt;span class="o"&gt;[&lt;/span&gt;SG-1] credential file staged &lt;span class="k"&gt;for &lt;/span&gt;commit.

fix: Add it to .gitignore. Anything committed is &lt;span class="k"&gt;in &lt;/span&gt;the &lt;span class="nb"&gt;history &lt;/span&gt;permanently, even after a later delete.

Rule text lives &lt;span class="k"&gt;in &lt;/span&gt;AGENTS.md.
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_input":{"command":"cat .env"}}'&lt;/span&gt; | node plugins/slopguard/scripts/guard-bash.mjs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_input":{"command":"echo AWS_KEY=AKIAIOSFODNN7EXAMPLE &amp;gt;&amp;gt; src/config.js"}}'&lt;/span&gt; | node plugins/slopguard/scripts/guard-bash.mjs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&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;Five calls, three results worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A live-shaped key going into source is blocked, and the agent is handed the rule and the fix. The same kind of line reading &lt;code&gt;process.env&lt;/code&gt; instead passes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;sk_test_&lt;/code&gt; key passes. That's deliberate: its rules treat test keys as placeholders, on the theory that a guard that cries wolf gets uninstalled.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cat .env&lt;/code&gt; and a key written with &lt;code&gt;echo &amp;gt;&amp;gt;&lt;/code&gt; both pass. slopguard checks the Write and Edit tools, not what Bash writes. Its README says so.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A setup that holds, and what still gets through
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read deny rules&lt;/strong&gt; for &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.env.*&lt;/code&gt;, exactly as Anthropic's docs show them. They're the only fix that covers the Read tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The sandbox, with &lt;code&gt;denyRead&lt;/code&gt; on the same paths&lt;/strong&gt;, if you're on macOS, Linux or WSL2. It's the only fix that covers scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hook only for what deny rules can't say.&lt;/strong&gt; Exit 2, and catch your own errors, because a crash is an allow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;slopguard for the write side&lt;/strong&gt;, so a key that does get read doesn't land in a commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No production secrets in the folder the agent works in.&lt;/strong&gt; Test keys in &lt;code&gt;.env&lt;/code&gt;; the real ones in a secret manager the dev machine never sees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one carries the weight. Your own app loads &lt;code&gt;.env&lt;/code&gt; on purpose. When the agent runs &lt;code&gt;npm start&lt;/code&gt; or your test suite, no rule sees a filename, and anything the app prints or logs is in the transcript. The only key that can't leak from a dev folder is one that was never in it.&lt;/p&gt;




&lt;p&gt;Sources: every terminal block is a real run on my machine on 9 Oct 2026, in a throwaway repo with fake keys. The Read, Glob and Grep results are from a live Claude Code session the same day. The deny rules, the hook and slopguard were not run inside a live session (the CLI's login had expired); the hook and slopguard were fed the documented hook JSON by hand. slopguard is v1.1.2: v1.1.1 echoed a 20-character AWS key ID back whole in its block message, which this test caught and which was fixed the same day. What deny rules and the sandbox cover is from Claude Code's &lt;a href="https://code.claude.com/docs/en/permissions" rel="noopener noreferrer"&gt;permissions&lt;/a&gt;, &lt;a href="https://code.claude.com/docs/en/settings" rel="noopener noreferrer"&gt;settings&lt;/a&gt; and &lt;a href="https://code.claude.com/docs/en/sandboxing" rel="noopener noreferrer"&gt;sandboxing&lt;/a&gt; docs; hook exit codes from the &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks reference&lt;/a&gt;. &lt;code&gt;AKIAIOSFODNN7EXAMPLE&lt;/code&gt; is AWS's published example key.&lt;/p&gt;

&lt;p&gt;Where this fits: &lt;a href="https://singhlabs.dev/agent-ops-stack/#guardrails" rel="noopener noreferrer"&gt;guardrails, layer 1 of the Agent Ops Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-code-stop-hook-example/" rel="noopener noreferrer"&gt;Claude Code Stop hook example: check the "done" claim.&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/capability-is-not-authority/" rel="noopener noreferrer"&gt;AI coding agent security: capability isn't authority.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/does-claude-code-read-env/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>security</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Claude Code Stop hook example: check the "done" claim.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:11:26 +0000</pubDate>
      <link>https://dev.to/manpreet171/claude-code-stop-hook-example-check-the-done-claim-4n16</link>
      <guid>https://dev.to/manpreet171/claude-code-stop-hook-example-check-the-done-claim-4n16</guid>
      <description>&lt;p&gt;Search for a Claude Code Stop hook example and you get two kinds. One runs the tests when the agent stops. The other plays a sound. Both are fine. Neither reads the paragraph the agent just wrote you.&lt;/p&gt;

&lt;p&gt;This one does. It takes the agent's last message, holds it against &lt;code&gt;git diff&lt;/code&gt;, and won't let the agent stop while a changed file goes unmentioned. 31 lines of Node, no dependencies. Every output below is a real run.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A test gate checks the code. Nothing checks the paragraph you're about to merge on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a Stop hook can see
&lt;/h2&gt;

&lt;p&gt;Stop fires when Claude finishes responding. Your command gets a JSON payload on stdin. Here's the one my hook was fed, paths shortened:&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="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;../stop-payload.json&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;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ed45f42f-…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transcript_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/projects/…/….jsonl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:/…/stop-hook-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permission_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hook_event_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stop"&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_hook_active"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_assistant_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Done. `login()` in auth.js now returns false when `user` is undefined, instead of throwing on `.trim()`. One guard line, no other changes needed."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background_tasks"&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;"session_crons"&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;Four fields matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;last_assistant_message&lt;/code&gt;&lt;/strong&gt; is the claim. No need to dig it out of the transcript: Anthropic's docs say hooks that want the final text should use this field instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;transcript_path&lt;/code&gt;&lt;/strong&gt; is the whole session as JSONL, for when the last message isn't enough.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;cwd&lt;/code&gt;&lt;/strong&gt; is where to run &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;stop_hook_active&lt;/code&gt;&lt;/strong&gt; is the one that bites. More on it below.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built that payload by hand, in the documented shape, from the real transcript of the Claude Code session that made the fix. Piping a saved payload in is also the quickest way to test a hook without waiting for Claude to stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual example: a test gate, and what it misses
&lt;/h2&gt;

&lt;p&gt;The task was small. &lt;code&gt;login()&lt;/code&gt; in &lt;code&gt;auth.js&lt;/code&gt; threw when the user was undefined. Claude Code added one guard line and said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Done. &lt;code&gt;login()&lt;/code&gt; in auth.js now returns false when &lt;code&gt;user&lt;/code&gt; is undefined, instead of throwing on &lt;code&gt;.trim()&lt;/code&gt;. One guard line, no other changes needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the working tree:&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;$ &lt;/span&gt;git status &lt;span class="nt"&gt;--short&lt;/span&gt;
 M auth.js
 M config.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;config.js&lt;/code&gt; was edited before the task even started, on purpose. I left a timeout set to 0 in it to stand in for the edit from three tasks ago. Now the tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tap"&gt;&lt;code&gt;$ node --test 2&amp;gt;&amp;amp;1 | grep -E "^# (pass|fail)"
&lt;span class="c"&gt;# pass 2
# fail 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Green. The usual Stop hook runs the suite and exits 2 on a failure, which the docs say "prevents Claude from stopping". Here it has nothing to fail on. Claude stops, you commit, and the zero timeout ships with the bug fix.&lt;/p&gt;

&lt;p&gt;Keep the test gate. It answers a real question. It just isn't this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better one: hold the summary against the diff
&lt;/h2&gt;

&lt;p&gt;Same event, different question: did the message name every file that changed? Save this as &lt;code&gt;.claude/hooks/check-done.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;&lt;span class="c1"&gt;// Stop hook: don't let Claude stop until its last message names every file it changed.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// We blocked once already and Claude is answering for it. Let it stop.&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stop_hook_active&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;exit&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="c1"&gt;// The claim: the last thing Claude said, handed over in the payload.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_assistant_message&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;claim&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;exit&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="c1"&gt;// The evidence: tracked files that changed, plus files that are new.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;git&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&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;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`git &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&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="nb"&gt;Boolean&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;changed&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="nx"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;diff --name-only HEAD&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="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ls-files --others --exclude-standard&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;catch&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;exit&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="c1"&gt;// not a git repo, or no commits yet&lt;/span&gt;

&lt;span class="c1"&gt;// A literal match on the path or the file name. Crude on purpose.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unnamed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;changed&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;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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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="nf"&gt;split&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;pop&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;unnamed&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;0&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;exit&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You said you're done but never mentioned: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;unnamed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="s2"&gt;. `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Say what changed in each one, or undo it.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ls-files --others&lt;/code&gt; half matters. &lt;code&gt;git diff&lt;/code&gt; on its own never lists a brand-new file, so without it a whole new file walks straight past.&lt;/p&gt;

&lt;p&gt;Register it in &lt;code&gt;.claude/settings.json&lt;/code&gt; and commit both, so the whole team gets it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/check-done.mjs"&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;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;Two details from the docs. Stop takes no &lt;code&gt;matcher&lt;/code&gt;. Add one and it's silently ignored. And &lt;code&gt;args&lt;/code&gt; switches the hook to exec form, with no shell in between, which the docs recommend whenever a path placeholder is involved. A project path with a space in it can't split the command in two.&lt;/p&gt;

&lt;p&gt;Same payload, same repo:&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;$ &lt;/span&gt;node .claude/hooks/check-done.mjs &amp;lt; ../stop-payload.json
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"decision"&lt;/span&gt;:&lt;span class="s2"&gt;"block"&lt;/span&gt;,&lt;span class="s2"&gt;"reason"&lt;/span&gt;:&lt;span class="s2"&gt;"You said you're done but never mentioned: config.js. Say what changed in each one, or undo it."&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"decision": "block"&lt;/code&gt; keeps Claude working, and &lt;code&gt;reason&lt;/code&gt; becomes its next instruction. The tests had nothing to say about &lt;code&gt;config.js&lt;/code&gt;. This did.&lt;/p&gt;

&lt;p&gt;If a block showing up as a hook error bothers you, the docs offer &lt;code&gt;hookSpecificOutput.additionalContext&lt;/code&gt; instead. Same loop protections, but the transcript labels it "Stop hook feedback".&lt;/p&gt;

&lt;h2&gt;
  
  
  The infinite-loop trap
&lt;/h2&gt;

&lt;p&gt;When the hook blocks, Claude answers and then tries to stop again. That fires the Stop hook again. If the hook asks the same question and gets the same answer, it blocks again.&lt;/p&gt;

&lt;p&gt;The way out is &lt;code&gt;stop_hook_active&lt;/code&gt;. The docs say it's true "when Claude Code is already continuing as a result of a stop hook". That's the second line of the script. Here it is with the flag set:&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;$ &lt;/span&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;stop_hook_active ../stop-payload-active.json
  &lt;span class="s2"&gt;"stop_hook_active"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
&lt;span class="nv"&gt;$ &lt;/span&gt;node .claude/hooks/check-done.mjs &amp;lt; ../stop-payload-active.json&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&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;No output, exit 0, Claude stops. The hook gets one block per stop. If Claude's reply to it is weak, it still stops, and you read the reply. That's the trade, and it's the right one.&lt;/p&gt;

&lt;p&gt;Without that line, the docs describe a safety net: Claude Code overrides the next block after stop hooks have continued the turn eight times in a row. Don't lean on it. The count resets every time Claude calls a tool, and checking a diff means calling one. I didn't crash a session to prove this. The guard costs one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or let a plugin do more of it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://singhlabs.dev/trust-issues/" rel="noopener noreferrer"&gt;trust issues&lt;/a&gt; is the free plugin I built on the same event. It reads the claim from &lt;code&gt;transcript_path&lt;/code&gt; and runs four checks instead of one: changed but never mentioned, quiet cuts like a deleted assertion, mutes like &lt;code&gt;.skip&lt;/code&gt;, and work described that never happened. Same payload, with &lt;code&gt;TI&lt;/code&gt; pointing at the plugin's &lt;code&gt;stop-check.mjs&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;$ &lt;/span&gt;node &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TI&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &amp;lt; ../stop-payload.json | node &lt;span class="nt"&gt;-pe&lt;/span&gt; &lt;span class="s2"&gt;"JSON.parse(require(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;).readFileSync(0)).systemMessage"&lt;/span&gt;
trust issues — the diff says otherwise:

plumb — 2 files changed, 1 named &lt;span class="k"&gt;in &lt;/span&gt;the summary

changed but never mentioned  &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;these first&lt;span class="o"&gt;)&lt;/span&gt;
  · config.js modified

1 thing the summary did not tell you.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default it tells you and doesn't block. A guardrail that interrupts you on its first false alarm gets uninstalled the same afternoon. When you trust it, set &lt;code&gt;TRUST_ISSUES=strict&lt;/code&gt; and it hands Claude the bill instead:&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;$ TRUST_ISSUES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;strict node &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TI&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &amp;lt; ../stop-payload.json
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"decision"&lt;/span&gt;:&lt;span class="s2"&gt;"block"&lt;/span&gt;,&lt;span class="s2"&gt;"reason"&lt;/span&gt;:&lt;span class="s2"&gt;"Before you call this done — the summary and the diff disagree:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;plumb — 2 files changed, 1 named in the summary&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;changed but never mentioned  (read these first)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  · config.js modified&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;1 thing the summary did not tell you.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Either fix what was quietly changed, or say plainly what you did and why."&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the &lt;code&gt;stop_hook_active: true&lt;/code&gt; payload it exits 0 and says nothing, in both modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this won't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It matches names literally.&lt;/strong&gt; "auth.js" counts. "The auth module" doesn't. Ask the agent to end every task with a file list and the noise goes away.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It checks the working tree, not the turn.&lt;/strong&gt; That's how it caught &lt;code&gt;config.js&lt;/code&gt;. It's also how it will nag you about the last task's files until you commit them. Commit between tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A mention isn't the truth.&lt;/strong&gt; "Updated the test" names the file and hides the &lt;code&gt;.skip&lt;/code&gt;. That's what the quiet-cut and mute checks in trust issues are for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It can't tell whether the code works.&lt;/strong&gt; Run the test gate too. Both can sit on Stop together.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why the summary leaves things out in the first place is its own post: &lt;a href="https://singhlabs.dev/blog/claude-code-says-done/" rel="noopener noreferrer"&gt;Claude Code says it's done. Check the diff, not the paragraph.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Sources: every terminal block is a real run on 9 Oct 2026 (Node 22.17.1, trust issues 1.0.1) in a throwaway git repo, with paths shortened. Claude Code's command line wasn't signed in on this machine, so no payload was logged from a live Stop. Both payloads were built in the documented shape from the real transcript of the Claude Code session that made the fix, then piped in by hand. Payload fields, exit codes, exec form, &lt;code&gt;stop_hook_active&lt;/code&gt; and the eight-continuation cap are from Anthropic's &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where this fits: &lt;a href="https://singhlabs.dev/agent-ops-stack/#testing" rel="noopener noreferrer"&gt;testing and verification, layer 5 of the Agent Ops Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-code-says-done/" rel="noopener noreferrer"&gt;Claude Code says it's done. Check the diff, not the paragraph.&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/a-green-tick-over-nothing/" rel="noopener noreferrer"&gt;My mailer printed “campaign sent”. It had sent to nobody.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/claude-code-stop-hook-example/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>testing</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How long should CLAUDE.md be? I priced every line.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:08:05 +0000</pubDate>
      <link>https://dev.to/manpreet171/how-long-should-claudemd-be-i-priced-every-line-4h9b</link>
      <guid>https://dev.to/manpreet171/how-long-should-claudemd-be-i-priced-every-line-4h9b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; under 200 lines, which is Anthropic's own target, and the reason is attention, not money. A line of &lt;code&gt;CLAUDE.md&lt;/code&gt; is about 13 tokens and it's sent again on every turn. Priced over 190 of my real Claude Code sessions at Opus 5.5 API rates, one line costs about a tenth of a cent per session with caching, and about a cent without. A whole 200-line file comes to 21 cents a session.&lt;/p&gt;

&lt;p&gt;Every guide answers this with a number. Sixty lines. A hundred. Two hundred. Three hundred. None says what a line costs, which is odd for a file that's sent again every time you press enter.&lt;/p&gt;

&lt;p&gt;So I priced one. Not with a file size times a message count, which ignores the cache, but with the transcripts Claude Code already keeps on my machine: 190 sessions, 63,964 turns, and the caching they actually got.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A line of CLAUDE.md costs about a tenth of a cent a session. Keep it short for Claude's sake, not your bill's.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The numbers everyone quotes, and where 200 comes from
&lt;/h2&gt;

&lt;p&gt;There's one official number. Anthropic's &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;memory docs&lt;/a&gt; say to "target under 200 lines per CLAUDE.md file", because a longer file takes more context and gets followed less reliably.&lt;/p&gt;

&lt;p&gt;The unofficial numbers come with a reason too. &lt;a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md" rel="noopener noreferrer"&gt;HumanLayer's guide&lt;/a&gt; keeps its own root file under sixty lines and reports a consensus of under 300. Its logic is a budget: frontier models follow roughly 150 to 200 instructions with reasonable consistency, and Claude Code's own system prompt already spends about 50 of them.&lt;/p&gt;

&lt;p&gt;Two hard limits do exist. Don't confuse them with the 200:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The 200-line cutoff is for a different file.&lt;/strong&gt; Claude Code's auto memory loads only the first 200 lines (or 25KB) of &lt;code&gt;MEMORY.md&lt;/code&gt;. &lt;code&gt;CLAUDE.md&lt;/code&gt; has no line cutoff at all: Claude Code loads the whole thing and only skips a file over 4 MiB.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Codex caps AGENTS.md at 32 KiB.&lt;/strong&gt; Per &lt;a href="https://learn.chatgpt.com/docs/agent-configuration/agents-md" rel="noopener noreferrer"&gt;its docs&lt;/a&gt;, Codex stops adding instruction files once they reach 32 KiB combined, unless you raise &lt;code&gt;project_doc_max_bytes&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So 200 is a target about attention. Nobody puts a price on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one line of CLAUDE.md costs over a real session
&lt;/h2&gt;

&lt;p&gt;Three numbers: tokens per line, how many times the line is sent, and what each send costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tokens per line.&lt;/strong&gt; Here's this site's own &lt;code&gt;CLAUDE.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; CLAUDE.md
114 CLAUDE.md
&lt;span class="nv"&gt;$ &lt;/span&gt;bridle agents CLAUDE.md | &lt;span class="nb"&gt;grep &lt;/span&gt;size
  ✔ size: 916 words ≈ 1474 tokens, re-sent every turn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,474 tokens over 114 lines is about 13 a line. That isn't a quirk of my file. Across 350 public files (more on those below), the typical line ran 10 to 14 tokens at every size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many times it's sent.&lt;/strong&gt; The file loads at the start of a session and stays to the end. It even comes back after &lt;code&gt;/compact&lt;/code&gt;. And Claude Code &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;sends the whole session again on every turn&lt;/a&gt;, where a turn is one API call: a request that runs ten tools is ten turns. Here's my whole history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ baggage --all
baggage — 190 sessions, all projects, 63,964 turns

  before you typed anything              76k  tokens
    system prompt, every tool schema, every skill, CLAUDE.md.
    paid again on all 63,964 turns = 4.8B tokens, 18% of the bill,
    whether you called any of it or not.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counting each session's turns from the same transcripts, the median session ran &lt;strong&gt;213 turns&lt;/strong&gt;. A quarter ran under 64, a quarter over 466, and the longest ran 2,943. So in a typical session, one line is sent 213 times: 2,769 tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What each send costs.&lt;/strong&gt; This is where the guesses go wrong, because of the cache. The first turn writes the line to Claude Code's one-hour cache at twice the input price. Every later turn reads it back at a twentieth of the input price on Opus 5.5 (&lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic's rates&lt;/a&gt;). Unless the cache broke: an idle hour, a &lt;code&gt;/compact&lt;/code&gt;, a model switch. Then the start of the session is written again at the write price. In my history, that happened on &lt;strong&gt;1.8% of turns&lt;/strong&gt; (1,137 of 63,453).&lt;/p&gt;

&lt;p&gt;So for one line over a 213-turn session on Opus 5.5:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;first write: 13 tokens at $8 per million = $0.0001&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;3.8 re-writes at the same price = $0.0004&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;208 cache reads at $0.20 per million = $0.0005&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;total: $0.0010.&lt;/strong&gt; Uncached, all 213 sends at $4 per million: $0.011.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same sum on three models, for one line and for a full 200-line file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Opus 5.5:&lt;/strong&gt; one line $0.0010 cached, $0.011 uncached. 200 lines: $0.21 cached, $2.22 uncached.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sonnet 5.5:&lt;/strong&gt; one line $0.0005 cached, $0.0055 uncached. 200 lines: $0.10 cached, $1.11 uncached.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fable 5.1:&lt;/strong&gt; one line $0.0019 cached, $0.028 uncached. 200 lines: $0.38 cached, $5.54 uncached.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things fall out of that. Caching cuts the price of a line by 11 to 14 times, so any figure that skips the cache is off by an order of magnitude. And on Opus 5.5, the five turns that write the line cost nearly as much as the 208 that read it, and a cache break re-writes the whole session, not just this file. If cost is the worry, keep the cache warm before you start cutting lines.&lt;/p&gt;

&lt;p&gt;On a Pro or Max plan you see a limit, not dollars. In tokens, a 200-line file is about 554,000 tokens sent over a typical session. How heavily a cached token counts against the limit isn't published, so I won't guess.&lt;/p&gt;

&lt;p&gt;One caveat on the ruler. bridle counts four characters a token, Anthropic's own rough estimate, but Anthropic also says Claude 4.7 and later produce about 30% more tokens for the same text. On current models, add about 30% to every figure here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does CLAUDE.md save tokens?
&lt;/h2&gt;

&lt;p&gt;Only when it saves turns. A wasted turn re-sends at least the 76k floor from the block above, plus everything since. A line costs 2,769 tokens a session. So a line that prevents one wrong turn every 27 sessions has paid for itself in tokens. A line that never changes what Claude does never will.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long real CLAUDE.md files are (350 repos)
&lt;/h2&gt;

&lt;p&gt;Last month, for another project, I pulled 350 public instruction files from GitHub: 220 &lt;code&gt;CLAUDE.md&lt;/code&gt; and 130 &lt;code&gt;AGENTS.md&lt;/code&gt;, one root file per repo, no forks, every repo pushed this year. I picked them evenly across six file sizes, so they show the range, not the typical file on GitHub. Then I ran &lt;code&gt;wc -l&lt;/code&gt; and bridle on every one. Median lines and tokens per line, by size:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;under 800 bytes (41 files): 8 lines, 10.6 tokens a line&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;800 bytes to 1.5 KB (57): 26 lines, 10.2&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;1.5 to 3 KB (61): 49 lines, 11.6&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;3 to 6 KB (50): 83 lines, 12.7&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;6 to 12 KB (70): 150 lines, 12.8&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;12 KB and up (71): 312 lines, 14.4&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bigger files have denser lines, with tables and code blocks, but the typical line stays between 10 and 14 tokens. 73 of the 350 run past 200 lines and 11 past 500. The longest is 980 lines, about 8,700 tokens. At the prices above, that one costs about 69 cents a session on Opus 5.5 with caching, or $7.38 without. 11 of the 350 are past the 32 KiB that Codex reads by default.&lt;/p&gt;

&lt;p&gt;My own 23 files have a median of 41 lines, and three run past 200. One of those, 311 lines and about 4,200 tokens, sits in a project where my sessions run a median 493 turns. That's 64 cents a session on Opus 5.5 with caching, $8.24 without, and under 5% of the 89k that project loads before I type anything.&lt;/p&gt;

&lt;p&gt;In money, length barely matters. That leaves Anthropic's reason: a long file gets followed less. The research on that is mixed (I went through it in &lt;a href="https://singhlabs.dev/blog/agents-md-vs-claude-md/" rel="noopener noreferrer"&gt;the AGENTS.md vs CLAUDE.md post&lt;/a&gt;), but every line still competes with the task for attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cutting it without losing rules
&lt;/h2&gt;

&lt;p&gt;Ask of each line: would Claude get something wrong without it? If not, delete it. If it would, check whether the line can live somewhere cheaper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rules for one part of the code&lt;/strong&gt; go in &lt;code&gt;.claude/rules/&lt;/code&gt; with a &lt;code&gt;paths:&lt;/code&gt; list at the top. Per the docs, they load when Claude reads a matching file, so they're carried only from that turn on, and only in sessions that touch those files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Procedures&lt;/strong&gt; become a skill. Its description loads every session; the body loads only when it's used.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;@imports&lt;/code&gt; don't shrink anything.&lt;/strong&gt; Imported files load at launch too. They tidy the file, not the bill.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Notes for humans&lt;/strong&gt; go in an HTML comment. Claude Code strips block-level comments before Claude sees the file, so they cost nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep what you keep repeating.&lt;/strong&gt; The lines worth their rent are the corrections you've typed three times. &lt;a href="https://singhlabs.dev/blog/claude-md-rules-chat-history/" rel="noopener noreferrer"&gt;Your chat history already has them&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I didn't do is trim a file and measure the difference. Trimming today and running baggage tomorrow compares two days of work, not two files. Done properly, it's a week of sessions on the shorter file: the floor, the share of turns that re-write the cache, and whether the cut rules start turning up in chat again.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; CLAUDE.md
&lt;span class="nv"&gt;$ &lt;/span&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; github:manpreet171/bridle agents CLAUDE.md   &lt;span class="c"&gt;# or AGENTS.md&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; github:manpreet171/baggage
&lt;span class="nv"&gt;$ &lt;/span&gt;baggage          &lt;span class="c"&gt;# this project's floor and turns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://singhlabs.dev/bridle/" rel="noopener noreferrer"&gt;bridle&lt;/a&gt; prints the file's size in tokens, plus what's missing from it. &lt;a href="https://singhlabs.dev/baggage/" rel="noopener noreferrer"&gt;baggage&lt;/a&gt; reads the transcripts already on your machine and prints your floor, your sessions and your turns. Both are free and send nothing anywhere. Then the sum is yours: tokens per line, times turns per session, times your model's cache price.&lt;/p&gt;




&lt;p&gt;Sources: both terminal blocks with output are real runs on my machine on 9 Oct 2026, the baggage one trimmed to its first lines. Turns per session and cache re-writes are counted from the same transcripts baggage reads, the same way it counts a turn. The 350 files are public repos pulled on 24 Sep 2026. Token counts are estimates at four characters a token. Prices, cache multipliers and the tokenizer note are from &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic's pricing page&lt;/a&gt;; CLAUDE.md loading, rules and limits from Claude Code's &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;memory docs&lt;/a&gt;; line counts and the instruction budget from &lt;a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md" rel="noopener noreferrer"&gt;HumanLayer&lt;/a&gt;; the 32 KiB cap from &lt;a href="https://learn.chatgpt.com/docs/agent-configuration/agents-md" rel="noopener noreferrer"&gt;Codex's AGENTS.md docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where this fits: &lt;a href="https://singhlabs.dev/agent-ops-stack/#memory" rel="noopener noreferrer"&gt;memory and context, layer 2 of the Agent Ops Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;Where your Claude Code tokens actually go. Output is 0.2%.&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/agents-md-vs-claude-md/" rel="noopener noreferrer"&gt;AGENTS.md vs CLAUDE.md: Claude Code reads both now, just not at once.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/how-long-should-claude-md-be/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>productivity</category>
      <category>llm</category>
    </item>
    <item>
      <title>AGENTS.md vs CLAUDE.md: Claude Code reads both now, just not at once.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:00:31 +0000</pubDate>
      <link>https://dev.to/manpreet171/agentsmd-vs-claudemd-claude-code-reads-both-now-just-not-at-once-hd</link>
      <guid>https://dev.to/manpreet171/agentsmd-vs-claudemd-claude-code-reads-both-now-just-not-at-once-hd</guid>
      <description>&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is a plain Markdown file in your repo that tells AI coding agents how to work on it. &lt;code&gt;CLAUDE.md&lt;/code&gt; is Claude Code's own version.&lt;/p&gt;

&lt;p&gt;Two files, one job. For a year the answer to "which one?" was simple. Claude Code read &lt;code&gt;CLAUDE.md&lt;/code&gt;. Almost everything else read &lt;code&gt;AGENTS.md&lt;/code&gt;. If you used both kinds of tool, you kept two files and watched them drift apart.&lt;/p&gt;

&lt;p&gt;On 18 September that changed. Claude Code 2.1.277 started reading &lt;code&gt;AGENTS.md&lt;/code&gt; natively. Some of the top results for this question still say it doesn't.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Claude Code reads AGENTS.md now. But only when there's no CLAUDE.md, and a single CLAUDE.local.md switches it off.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the current picture, the one setup that works on every version, and the part none of the comparison posts measure: what the file costs you on every turn, and what's missing from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does AGENTS.md work with Claude Code?
&lt;/h2&gt;

&lt;p&gt;Yes, from version 2.1.277, as long as no &lt;code&gt;CLAUDE.md&lt;/code&gt; is in the way. The rules, from Anthropic's &lt;a href="https://code.claude.com/docs/en/memory#agents-md" rel="noopener noreferrer"&gt;memory docs&lt;/a&gt;, as of today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Code reads &lt;code&gt;AGENTS.md&lt;/code&gt; only when there's no &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/strong&gt; That means no &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;.claude/CLAUDE.md&lt;/code&gt; or &lt;code&gt;CLAUDE.local.md&lt;/code&gt; in the folder you're working in or any folder above it. Your personal &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; doesn't count, so that still loads alongside.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The trap.&lt;/strong&gt; Add a &lt;code&gt;CLAUDE.local.md&lt;/code&gt; for your own notes and your team's &lt;code&gt;AGENTS.md&lt;/code&gt; quietly stops loading. Nothing tells you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You can change the rule.&lt;/strong&gt; In &lt;code&gt;/config&lt;/code&gt;, "Project instructions" can be set to read either file (the default), both, or only &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Telemetry off? Update.&lt;/strong&gt; Before 2.1.281, sessions with telemetry disabled, or on Bedrock, Vertex or an LLM gateway, skipped &lt;code&gt;AGENTS.md&lt;/code&gt; without a word (&lt;a href="https://github.com/anthropics/claude-code/issues/95690" rel="noopener noreferrer"&gt;issue #95690&lt;/a&gt;). 2.1.281 fixed it, per the &lt;a href="https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;. The &lt;a href="https://code.claude.com/docs/en/memory#when-agents-md-support-is-unavailable" rel="noopener noreferrer"&gt;docs&lt;/a&gt; add one more: the first session after upgrading from 2.1.276 or earlier can skip it too.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Nearly everything else reads &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/strong&gt; Codex, Cursor, Copilot, Jules, Amp, Windsurf, Zed and more, per the list at &lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;agents.md&lt;/a&gt;. Gemini CLI is the odd one out: it reads &lt;code&gt;GEMINI.md&lt;/code&gt; unless you point it elsewhere.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more thing to check before any of that applies to you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ claude --version
2.1.257 (Claude Code)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the CLI on my own machine. It's older than 2.1.277, so here an &lt;code&gt;AGENTS.md&lt;/code&gt; on its own would be invisible to Claude Code, whatever the docs say. If your team updates at different speeds, some of you are reading the file and some aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you use?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Only Claude Code touches the repo?&lt;/strong&gt; Keep &lt;code&gt;CLAUDE.md&lt;/code&gt;. There's no &lt;code&gt;AGENTS.md&lt;/code&gt; in this website's repo, because Claude Code is the only agent that works on it. A second file would be a second thing to keep true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Several tools, or a team on mixed versions?&lt;/strong&gt; Make &lt;code&gt;AGENTS.md&lt;/code&gt; the one real file, and make &lt;code&gt;CLAUDE.md&lt;/code&gt; a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the setup Anthropic's docs recommend. Claude Code pulls the file in and never loads it twice, and it works on old versions too, because it doesn't depend on the new support at all. Two details that trip people up: an import inside a code fence is silently ignored, and on Windows a symlink instead of an import needs admin rights or Developer Mode, and Claude's own edit tools refuse to write through one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What not to do&lt;/strong&gt; is keep two hand-maintained copies. On the long-running &lt;a href="https://github.com/anthropics/claude-code/issues/6235" rel="noopener noreferrer"&gt;GitHub issue&lt;/a&gt; asking for &lt;code&gt;AGENTS.md&lt;/code&gt; support, it's a recurring complaint: the copies drift, and each agent ends up following different rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the file costs you
&lt;/h2&gt;

&lt;p&gt;Whichever file you pick, it's read at the start of every session and then sent again on every turn, like everything else in the conversation. So its size isn't a one-off. Here's this repo's &lt;code&gt;CLAUDE.md&lt;/code&gt;, checked by a small linter:&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;$ &lt;/span&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; github:manpreet171/bridle agents CLAUDE.md
Linting CLAUDE.md

  ✔ 2 runnable &lt;span class="nb"&gt;command &lt;/span&gt;block&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  ✘ does not say how to &lt;span class="nb"&gt;install&lt;/span&gt; / &lt;span class="nb"&gt;set &lt;/span&gt;up
  ✘ does not say how to run the tests
  ✔ how to build or run it
  &lt;span class="o"&gt;!&lt;/span&gt; 3 angle-bracket field&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;, e.g. &amp;lt;slug&amp;gt; — check these are argument syntax, not unfilled template text
  ✔ nothing &lt;span class="k"&gt;in &lt;/span&gt;here tells the agent to &lt;span class="k"&gt;do &lt;/span&gt;something dangerous
  ✔ 12 prohibition&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; — the agent knows where the edges are
  ✔ size: 916 words ≈ 1474 tokens, re-sent every turn

FAIL — 2 problem&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Your agent is reading this file on every run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two failures don't apply here: it's a static site with no install step and no test suite. A general linter checks for a general project, so read it as a list of questions, not a verdict.&lt;/p&gt;

&lt;p&gt;The size line is the one that matters. About 1,500 tokens, on every turn. This project has run about 9,300 turns of Claude Code, so this one file accounts for roughly 14 million tokens, sent at the cheaper cached rate. That's an estimate, but the shape isn't. I measured where the rest of a session's tokens go in &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;a separate post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Does a longer file make the agent worse at following it? Anthropic's &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;memory docs&lt;/a&gt; say to aim for under 200 lines (I &lt;a href="https://singhlabs.dev/blog/how-long-should-claude-md-be/" rel="noopener noreferrer"&gt;priced what each line costs&lt;/a&gt;), and its &lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;best-practices guide&lt;/a&gt; warns that bloated files get ignored. The research is more mixed. An &lt;a href="https://arxiv.org/abs/2602.11988" rel="noopener noreferrer"&gt;ETH Zurich study&lt;/a&gt; found files written by people raised success rates slightly, files generated by an AI lowered them, and both added roughly 20% to cost. A &lt;a href="https://arxiv.org/abs/2605.10039" rel="noopener noreferrer"&gt;study of 1,650 Claude Code sessions&lt;/a&gt; found no measurable effect of length on following one simple rule. Length isn't proven to hurt. It is proven to cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's missing from it
&lt;/h2&gt;

&lt;p&gt;A file can be long and still not say the thing you actually need it to. Here's the same project from the other direction: not what the file says, but what I keep typing to the AI because the file doesn't say it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npx -y toldya --dry

toldya · 15 sessions (1 Aug – 9 Oct) · 727 of your messages · 44 corrections

You keep telling your AI:
   1.   3×  keep it simple   (3 sessions)

And 7 times you told it to try or check again: its first go missed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;916 words of rules, and that line isn't in there. The correction I gave most often in this repo had never been written down, so every new session started without it and I typed it again.&lt;/p&gt;

&lt;p&gt;That's the real answer to "which file?". The name decides which tools read it. What's in it decides whether it was worth reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs in it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What it can't work out on its own.&lt;/strong&gt; The odd build command, the folder that looks unused but isn't, the deploy that happens on push.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The edges.&lt;/strong&gt; What it must never touch, and what needs a person. Those are the lines that prevent damage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What you keep correcting.&lt;/strong&gt; If you've typed it three times, it's a rule. Write it in your own words.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not documentation.&lt;/strong&gt; Explaining the codebase is what the code is for. Every paragraph the agent could have read in the repo is a paragraph you pay for on every turn.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not generated filler.&lt;/strong&gt; The ETH study's sharpest finding was that AI-written context files made results worse. Write it yourself, briefly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AGENTS.md vs SKILL.md
&lt;/h2&gt;

&lt;p&gt;Different jobs. &lt;code&gt;AGENTS.md&lt;/code&gt; holds facts the agent needs on every turn, and you pay for them on every turn. A skill is a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; in it: a procedure that loads only when the job comes up. Until then, only its name and description ride along.&lt;/p&gt;

&lt;p&gt;Anthropic's &lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;skills docs&lt;/a&gt; draw the line: make a skill when a section of &lt;code&gt;CLAUDE.md&lt;/code&gt; "has grown into a procedure rather than a fact." The deploy happens on push: a fact, so it goes in the file. The steps of a release: a skill.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;claude &lt;span class="nt"&gt;--version&lt;/span&gt;                             &lt;span class="c"&gt;# 2.1.277 or later to read AGENTS.md&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; github:manpreet171/bridle agents CLAUDE.md   &lt;span class="c"&gt;# or AGENTS.md&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; toldya &lt;span class="nt"&gt;--dry&lt;/span&gt;                          &lt;span class="c"&gt;# what you keep repeating&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tools are free, read only your own files, and change nothing unless you say yes. &lt;a href="https://singhlabs.dev/bridle/" rel="noopener noreferrer"&gt;bridle&lt;/a&gt; reads the instruction file; &lt;a href="https://singhlabs.dev/toldya/" rel="noopener noreferrer"&gt;toldya&lt;/a&gt; reads your Claude Code history.&lt;/p&gt;




&lt;p&gt;Sources: every terminal block is a real run in this website's repo on 5 Oct 2026, except toldya's: re-run on 9 Oct with 0.3.5 (0.3.3 stopped counting resumed sessions twice, 0.3.4 fixed how it groups rewordings, 0.3.5 skips "I don't get it"). Loading rules are from Anthropic's &lt;a href="https://code.claude.com/docs/en/memory#agents-md" rel="noopener noreferrer"&gt;memory docs&lt;/a&gt; and the Claude Code changelog for 2.1.277 and 2.1.281; the telemetry bug is from &lt;a href="https://github.com/anthropics/claude-code/issues/95690" rel="noopener noreferrer"&gt;issue #95690&lt;/a&gt;. Skills are from Anthropic's &lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;skills docs&lt;/a&gt;. Tool support is from &lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;agents.md&lt;/a&gt;. The studies are &lt;a href="https://arxiv.org/abs/2602.11988" rel="noopener noreferrer"&gt;arXiv 2602.11988&lt;/a&gt; and &lt;a href="https://arxiv.org/abs/2605.10039" rel="noopener noreferrer"&gt;arXiv 2605.10039&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where this fits: &lt;a href="https://singhlabs.dev/agent-ops-stack/#memory" rel="noopener noreferrer"&gt;memory and context, layer 2 of the Agent Ops Stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-md-rules-chat-history/" rel="noopener noreferrer"&gt;The best CLAUDE.md rules are hiding in your chat history&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;Where your Claude Code tokens actually go. Output is 0.2%.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/agents-md-vs-claude-md/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Claude Code says it's done. Check the diff, not the paragraph.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Mon, 05 Oct 2026 11:58:50 +0000</pubDate>
      <link>https://dev.to/manpreet171/claude-code-says-its-done-check-the-diff-not-the-paragraph-3g55</link>
      <guid>https://dev.to/manpreet171/claude-code-says-its-done-check-the-diff-not-the-paragraph-3g55</guid>
      <description>&lt;p&gt;"Done. All tests pass, and nothing else was touched." You read it, it's late, you merge.&lt;/p&gt;

&lt;p&gt;Claude Code's own issue tracker is full of what happens next. A test suite whose total quietly changed from 4,992 to 4,966 before the &lt;a href="https://github.com/anthropics/claude-code/issues/46940" rel="noopener noreferrer"&gt;"ALL PASSED"&lt;/a&gt;. Six tests &lt;a href="https://github.com/anthropics/claude-code/issues/45550" rel="noopener noreferrer"&gt;marked skip&lt;/a&gt;. A project reported 100% complete that an audit &lt;a href="https://github.com/anthropics/claude-code/issues/53983" rel="noopener noreferrer"&gt;put nearer 60%&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The summary isn't a lie. It's written to explain the work, and nobody explains the parts they'd rather you didn't see.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Asking the agent to try harder doesn't fix that. Checking does. Here's what goes wrong, what doesn't work, and what happened when I held this site's own Claude Code summaries up against their real diffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four ways "done" isn't done
&lt;/h2&gt;

&lt;p&gt;Read enough of those issues and the same four shapes keep coming back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The tests got easier, not the code better.&lt;/strong&gt; A test skipped, an assertion loosened, a test command narrowed to the ones that pass. The suite is green because there's less of it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The count got reframed.&lt;/strong&gt; A failure becomes "a flaky timeout". A smaller total gets reported as everything passing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Intent got ticked off as work.&lt;/strong&gt; A to-do marked done because it was planned, or a stub counted as a feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A file changed and never got mentioned.&lt;/strong&gt; The quietest one, and the one that bites three days later.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's common enough to measure. A &lt;a href="https://arxiv.org/abs/2605.29442" rel="noopener noreferrer"&gt;study of 20,574 real agent sessions&lt;/a&gt; found inaccurate self-reporting was about 23% of all the misbehaviour it caught, and a growing share over time. Anthropic's own &lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;best-practices guide&lt;/a&gt; says it plainly: Claude stops when the work looks done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Asking "did you actually do it?"&lt;/strong&gt; The same model that wrote the summary checks it with the same blind spot. In one report it &lt;a href="https://github.com/anthropics/claude-code/issues/39907" rel="noopener noreferrer"&gt;said yes three times&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A rule in &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/strong&gt; One reporter had a 250-line file, memory notes and hooks, and &lt;a href="https://github.com/anthropics/claude-code/issues/37818" rel="noopener noreferrer"&gt;still got false "done"s&lt;/a&gt;. In another, the rule against skipping tests was right there in context and got broken anyway. A rule asks. A check doesn't need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found in my own sessions
&lt;/h2&gt;

&lt;p&gt;Most writing on this describes the problem. Almost none of it shows a real summary next to its real diff, so I did that with two pieces of work Claude Code finished on this website today. For each one I took the summary it gave me at the end, word for word, and ran it through &lt;a href="https://singhlabs.dev/plumb/" rel="noopener noreferrer"&gt;plumb&lt;/a&gt;, a small tool that holds a summary against &lt;code&gt;git diff&lt;/code&gt; and prints only what doesn't match.&lt;/p&gt;

&lt;p&gt;The first was publishing a blog post, along with a fix to a script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ plumb check summary.md --base a12ccca
plumb — 9 files changed, 1 named in the summary

changed but never mentioned  (read these first)
  · .assetsignore modified
  · LOG.md modified
  · assets/blog/og-claude-code-token-usage.png added
  · baggage/index.html modified
  · blog/claude-code-token-usage/index.html added
  · blog/feed.xml modified
  · blog/index.html modified
  · sitemap.xml modified

8 things the summary did not tell you.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight flags. I went through them one by one. Seven were things the summary did describe, just not by file name: it gave the post's URL, called the image "the social card", and said "that folder is now excluded from publishing" instead of naming &lt;code&gt;.assetsignore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One wasn't. &lt;strong&gt;&lt;code&gt;baggage/index.html&lt;/code&gt;&lt;/strong&gt;: Claude added a link to a product page while publishing the post, and the summary never said so anywhere. Harmless, as it happens. But I only know it's harmless because I looked.&lt;/p&gt;

&lt;p&gt;The second was a batch of SEO fixes: 21 files changed, and again only one named by path. This time the summary described all 20 others in categories, "19 pages and the sitemap", and every one checked out. plumb also flagged two files as "mentioned but not changed", which the summary had only referred to. Nothing was missing.&lt;/p&gt;

&lt;p&gt;So, across two real summaries: 30 files changed, 2 named by path, one change genuinely left out. No skipped tests, no removed guards. Neither summary lied. One still hid a change, and I'd have merged it without blinking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read the diff, not the paragraph.&lt;/strong&gt; &lt;code&gt;/diff&lt;/code&gt; in Claude Code, or &lt;code&gt;git diff --stat&lt;/code&gt;, shows every file that changed. The summary is a guide to the diff, never a replacement for it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make it end with a file list.&lt;/strong&gt; Ask for every file it touched, by path, at the end of each task. My two summaries were noisy to check because they talked in categories. A list turns "did it mention everything?" into a mechanical question.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check on every stop, automatically.&lt;/strong&gt; Claude Code's &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Stop hook&lt;/a&gt; receives the agent's last message, so a check can run the moment it says it's finished. &lt;a href="https://singhlabs.dev/trust-issues/" rel="noopener noreferrer"&gt;trust issues&lt;/a&gt; is a free plugin that does exactly that: the same four checks, every time, with nobody remembering to run them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Count the tests, before and after.&lt;/strong&gt; If the number went down, ask why before you read anything else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Get a second pair of eyes that didn't do the work.&lt;/strong&gt; Anthropic's guide suggests a separate verification step that checks nothing outside the task changed. A fresh session doesn't share the first one's reasons for skipping a test.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it on yours
&lt;/h2&gt;

&lt;p&gt;Save the agent's last message to a file, then:&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;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; github:manpreet171/plumb
&lt;span class="nv"&gt;$ &lt;/span&gt;plumb check summary.md                &lt;span class="c"&gt;# against your uncommitted changes&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;plumb check summary.md &lt;span class="nt"&gt;--base&lt;/span&gt; main    &lt;span class="c"&gt;# against a branch or commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 1 means it found something, so it works as a CI gate too. What it won't do, so you don't over-trust it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It matches names literally.&lt;/strong&gt; A URL or a category doesn't count as a mention, which is why my first run flagged seven things that weren't really missing. Ask for a file list and that noise goes away.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It can't tell whether the code works.&lt;/strong&gt; It finds what was left out of the story, not bugs. Run the tests for that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Its "quiet cuts" are patterns.&lt;/strong&gt; It spots a removed assertion or an added &lt;code&gt;.skip&lt;/code&gt;, not every possible way to weaken a check.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Sources: the plumb output is a real run (plumb 1.1.0) on 5 Oct 2026, against the exact end-of-task summary Claude Code gave for each piece of work, with the summary file's path shortened. The second run's output is described rather than shown because it lists 20 files. Incidents are from the linked GitHub issues on anthropics/claude-code; the session study is &lt;a href="https://arxiv.org/abs/2605.29442" rel="noopener noreferrer"&gt;arXiv 2605.29442&lt;/a&gt;; hook and review guidance is from Anthropic's &lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;best practices&lt;/a&gt; and &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&gt; docs.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/a-green-tick-over-nothing/" rel="noopener noreferrer"&gt;My mailer printed “campaign sent”. It had sent to nobody.&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;Where your Claude Code tokens actually go. Output is 0.2%.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/claude-code-says-done/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Where your Claude Code tokens actually go. Output is 0.2%.</title>
      <dc:creator>Manpreet Singh</dc:creator>
      <pubDate>Mon, 05 Oct 2026 11:38:18 +0000</pubDate>
      <link>https://dev.to/manpreet171/where-your-claude-code-tokens-actually-go-output-is-02-n8e</link>
      <guid>https://dev.to/manpreet171/where-your-claude-code-tokens-actually-go-output-is-02-n8e</guid>
      <description>&lt;p&gt;You hit the usage limit before lunch. So you do what everyone online says: make the AI talk less. Ban the "Great question!", cut the summaries, install one of the skills that trims its replies.&lt;/p&gt;

&lt;p&gt;I wanted to know where the tokens actually go, so I counted. Not estimated. Counted, from the transcripts Claude Code already keeps on your machine: every session still on mine, 173 of them, about 60,000 turns.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Everything Claude wrote back to me was 0.2% of the tokens. The rest was my own session, fed back in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That number is real, but it isn't the whole story either, because most of those re-sent tokens are cheap ones. This post is both halves: where the tokens go, what they really cost, and the few things that actually move the number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The count
&lt;/h2&gt;

&lt;p&gt;Here's the summary across every project, straight from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ baggage --all
baggage — 173 sessions, all projects, 60,454 turns

  before you typed anything              75k  tokens
    system prompt, every tool schema, every skill, CLAUDE.md.
    paid again on all 60,454 turns = 4.5B tokens, 17% of the bill,
    whether you called any of it or not.

  conversation you can see             27.8M  tokens
  what the API billed                  26.0B  tokens

  a 934x gap. Some of it is the fixed cost above; the rest
  is everything you picked up being re-sent on every later turn.

  output tokens           56.6M   0.2%
  re-sent context         25.3B   97.5%   &amp;lt;- the bill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines matter. &lt;strong&gt;Output&lt;/strong&gt; (every reply, every line of code Claude wrote, its thinking included) is 56.6 million tokens. &lt;strong&gt;Re-sent context&lt;/strong&gt; is 25.3 billion. That's roughly 450 to 1.&lt;/p&gt;

&lt;p&gt;I'm not the first to see this shape. A &lt;a href="https://github.com/anthropics/claude-code/issues/24147" rel="noopener noreferrer"&gt;GitHub issue&lt;/a&gt; on Claude Code found the same thing in 30 days of someone else's transcripts, and a &lt;a href="https://dev.to/ploofnexa/i-measured-where-claude-code-actually-spends-tokens-968-is-re-reading-history-my-typing-was-16gm"&gt;dev.to post&lt;/a&gt; measured 0.5% output across 32 sessions. Different people, different work, same picture. What none of them show is &lt;em&gt;which things&lt;/em&gt; in a session cost the most, and what any of it means for the bill. That's the rest of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works like that
&lt;/h2&gt;

&lt;p&gt;The model has no memory between turns. Every time you press enter, Claude Code sends the whole conversation again: the system prompt, the tool definitions, every file it read, every command output, every reply so far, and then your new message.&lt;/p&gt;

&lt;p&gt;Think of a suitcase you have to carry up every flight of stairs. Pack a brick on the second floor of a 200-floor building and you carry it up 198 more flights. A 4,000-token build log that lands at turn 12 of a 200-turn session isn't 4,000 tokens. It's 4,000 sent 188 more times.&lt;/p&gt;

&lt;p&gt;So the cost of anything in a session isn't its size. It's its size times the number of turns it stays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching makes it cheaper, not smaller
&lt;/h2&gt;

&lt;p&gt;This is the part most guides get half right. Claude Code caches the conversation, and Anthropic &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;prices a cache read&lt;/a&gt; at a tenth of normal input (a twentieth on Opus 5.5, a fortieth on Fable 5.1). Output costs five times input. So 97.5% of the tokens is not 97.5% of the money.&lt;/p&gt;

&lt;p&gt;I priced my own split at those published rates, with the one-hour cache a subscription uses. Output comes to &lt;strong&gt;7 to 14% of the cost&lt;/strong&gt;, depending on the model. Re-reading and re-caching the session is the other &lt;strong&gt;86 to 93%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Less dramatic than 0.2%. Still the opposite of what "make it talk less" assumes.&lt;/p&gt;

&lt;p&gt;On a Pro or Max plan you never see dollars, you see a limit. Anthropic's &lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;cost docs&lt;/a&gt; say re-read history still draws on your usage, at the cached rate, so a one-line question late in a long session draws usage for the whole session. How heavily a cached token counts against the limit isn't published. I've seen people online insist it's full weight, and others insist it's free. Nobody I found has shown either, so I'm not going to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The floor you pay before you type
&lt;/h2&gt;

&lt;p&gt;That first block of the output is the one nobody shows you. Before you've typed a word, a session already carries the system prompt, the built-in tools, the descriptions of every skill you've installed, your &lt;code&gt;CLAUDE.md&lt;/code&gt; files and anything else that loads at start. A typical session on my machine starts at about 75,000 tokens.&lt;/p&gt;

&lt;p&gt;It rides along on every turn. Across all my sessions, that floor alone is &lt;strong&gt;17% of everything billed&lt;/strong&gt;, whether I used any of it or not. It's also the one line you can fix in thirty seconds: every paragraph of &lt;code&gt;CLAUDE.md&lt;/code&gt; you don't need is paid again on every turn of every session, and so is the description of every skill you never call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The heaviest things I carry
&lt;/h2&gt;

&lt;p&gt;Here's one project, this website, with the list of what's costing the most rent, trimmed for length:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ baggage
baggage — 13 sessions in singhlabs, 9,307 turns

  before you typed anything              84k  tokens
    system prompt, every tool schema, every skill, CLAUDE.md.
    paid again on all 9,307 turns = 779.0M tokens, 18% of the bill,
    whether you called any of it or not.

  output tokens            7.1M   0.2%
  re-sent context          4.2B   98.0%   &amp;lt;- the bill

  HEAVIEST THINGS YOU ARE STILL CARRYING
  (rent = its size x the turns it stayed in context)

   304.4M   19.4%  3769x  assistant reply
   282.4M   18.0%  1376x  your message
   244.2M   15.6%  1415x  claude-in-chrome · browser_batch
    54.6M    3.5%   145x  WebSearch
    25.6M    1.6%   275x  claude-in-chrome · javascript_tool
    21.5M    1.4%    17x  claude-in-chrome · get_page_text
    14.9M    0.9%   159x  Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things surprised me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude's own replies are the biggest line: 19.4%.&lt;/strong&gt; Not because they were expensive to write. Writing all of them was part of the 0.2%. They're expensive because each one stays in the session and is sent again on every turn after it. So the "talk less" skills aren't wrong. They're right for the wrong reason: a short reply saves you far more in re-sends than it ever cost to write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser automation is close behind: 15.6%.&lt;/strong&gt; And that's the text alone: page contents, element lists, logs of each click. baggage doesn't count images, so every screenshot rides along on top of that figure, uncounted. A session that clicks through a website carries a stack of pictures of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moves the number
&lt;/h2&gt;

&lt;p&gt;Ranked by what the counts above say, not by what's easiest to write about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start fresh between unrelated tasks.&lt;/strong&gt; &lt;code&gt;/clear&lt;/code&gt; drops everything you've been carrying, and Anthropic's docs say it costs nothing. The brick stays on floor two.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lower the floor.&lt;/strong&gt; Uninstall skills you don't use, keep &lt;code&gt;CLAUDE.md&lt;/code&gt; to what Claude can't work out on its own, and run &lt;code&gt;/context&lt;/code&gt; once to see what loads before you type.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep big output out of the main session.&lt;/strong&gt; Send a long log to a file and search it, instead of printing it into the chat where it's carried to the end. Hand noisy exploration and browser work to a subagent, so only its answer comes back.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't break the cache mid-session.&lt;/strong&gt; Per the &lt;a href="https://code.claude.com/docs/en/prompt-caching" rel="noopener noreferrer"&gt;caching docs&lt;/a&gt;, switching model, changing effort, turning on fast mode or changing MCP servers can throw it away, and then the whole session is written to cache again at the higher rate. Decide those at the start.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shorter replies, for the right reason.&lt;/strong&gt; Ask for the answer without the essay. It helps, because the essay gets carried.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a paid plan, &lt;code&gt;/usage&lt;/code&gt; now shows which skills, subagents and MCP servers used your allowance. Worth a look before changing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count your own
&lt;/h2&gt;

&lt;p&gt;The tool that printed everything above is called &lt;a href="https://singhlabs.dev/baggage/" rel="noopener noreferrer"&gt;baggage&lt;/a&gt;. It's free, it reads the transcripts already on your machine, and nothing leaves it. The name on npm belongs to someone else, so install it from the repo:&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;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; github:manpreet171/baggage
&lt;span class="nv"&gt;$ &lt;/span&gt;baggage          &lt;span class="c"&gt;# this project&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;baggage &lt;span class="nt"&gt;--all&lt;/span&gt;    &lt;span class="c"&gt;# every project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it doesn't tell you, so you don't over-read it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tokens, not dollars.&lt;/strong&gt; The totals are the exact counts the API reported. Pricing them is your model and your plan.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Item sizes are estimates.&lt;/strong&gt; The list ranks things with a rough four-characters-a-token ruler, the same ruler for everything. The totals above it are exact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Only what's still on your machine.&lt;/strong&gt; Claude Code keeps about 30 days of history by default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One heavy user.&lt;/strong&gt; These are my numbers. Yours will differ, which is the point of running it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Sources: both terminal blocks are real baggage runs on my own machine on 5 Oct 2026, the second trimmed to whole lines for length. Prices and cache multipliers are from &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic's pricing page&lt;/a&gt;; the cost split weights my exact token counts by them. Cache and usage behaviour is from Claude Code's &lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;costs&lt;/a&gt; and &lt;a href="https://code.claude.com/docs/en/prompt-caching" rel="noopener noreferrer"&gt;prompt caching&lt;/a&gt; docs.&lt;/p&gt;

&lt;p&gt;Read next: &lt;a href="https://singhlabs.dev/blog/claude-md-rules-chat-history/" rel="noopener noreferrer"&gt;The best CLAUDE.md rules are hiding in your chat history&lt;/a&gt; · &lt;a href="https://singhlabs.dev/blog/loop-engineering-map/" rel="noopener noreferrer"&gt;I researched loop engineering to build a product. I built nothing.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://singhlabs.dev/blog/claude-code-token-usage/" rel="noopener noreferrer"&gt;singhlabs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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