<?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: elijahmanlockedin112</title>
    <description>The latest articles on DEV Community by elijahmanlockedin112 (@elijahmanlockedin112).</description>
    <link>https://dev.to/elijahmanlockedin112</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%2F4144513%2F58c621a3-95b1-41c5-b99f-7f201d74d0c1.png</url>
      <title>DEV Community: elijahmanlockedin112</title>
      <link>https://dev.to/elijahmanlockedin112</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elijahmanlockedin112"/>
    <language>en</language>
    <item>
      <title>Claude Code said "Done." My tests said otherwise. Here's the 20-line fix.</title>
      <dc:creator>elijahmanlockedin112</dc:creator>
      <pubDate>Sat, 26 Sep 2026 15:08:37 +0000</pubDate>
      <link>https://dev.to/elijahmanlockedin112/claude-code-said-done-my-tests-said-otherwise-heres-the-20-line-fix-jc</link>
      <guid>https://dev.to/elijahmanlockedin112/claude-code-said-done-my-tests-said-otherwise-heres-the-20-line-fix-jc</guid>
      <description>&lt;p&gt;If you use Claude Code, you've seen this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude: Done! Partial refunds are implemented. ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you run the tests yourself and four of them fail.&lt;/p&gt;

&lt;p&gt;Claude isn't lying. It stops when the work &lt;em&gt;looks&lt;/em&gt; done, because it has no other signal. If nothing tells it the tests are red, "looks done" is all it has. So you become the test runner: you check, paste the errors back, wait, and check again.&lt;/p&gt;

&lt;p&gt;This post shows how to make Claude run that check itself, every time, before it's allowed to say it's finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the highest-leverage fix
&lt;/h2&gt;

&lt;p&gt;Anthropic's &lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;best practices for Claude Code&lt;/a&gt; put this first: give Claude a way to verify its work, such as tests, a build, a linter or a script, anything that returns pass or fail. Boris Cherny, who created Claude Code, &lt;a href="https://x.com/bcherny/status/2007179832300581177" rel="noopener noreferrer"&gt;has said&lt;/a&gt; that this feedback loop improves the quality of the result 2–3×.&lt;/p&gt;

&lt;p&gt;There are three places you can put that check:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;How reliable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In your prompt ("run the tests after")&lt;/td&gt;
&lt;td&gt;Works when you remember to type it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Advice. Claude usually follows it, sometimes not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In a &lt;strong&gt;hook&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Runs every time. Claude can't skip it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hooks are scripts Claude Code runs at fixed points in its loop. The one we want is the &lt;strong&gt;Stop&lt;/strong&gt; hook, which runs whenever Claude tries to end its turn. If a Stop hook exits with code &lt;strong&gt;2&lt;/strong&gt;, Claude Code doesn't let the turn end. It sends whatever the hook printed to stderr back to Claude, and Claude keeps working.&lt;/p&gt;

&lt;p&gt;That's the whole trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 20-line version
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;.claude/hooks/verify-gate.py&lt;/code&gt; in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getcwd&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sys&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;# no check configured: let Claude stop
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;consecutive_blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&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;# blocked 4 times in a row: let Claude stop instead of looping
&lt;/span&gt;
&lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&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="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verification failed:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Fix the failures above, then finish. Do not weaken or skip tests.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&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="c1"&gt;# exit 2 = block the stop; stderr is sent back to Claude
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it in &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"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;"python3 &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${CLAUDE_PROJECT_DIR}/.claude/hooks/verify-gate.py&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;330&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;(On Windows, &lt;code&gt;python&lt;/code&gt; instead of &lt;code&gt;python3&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;Then put your check in &lt;code&gt;.claude/verify.txt&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;npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Claude Code. The next time it says "Done" with red tests, this is what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude: Done. Partial refunds are implemented.
Stop hook: Verification failed:
  ✗ refund.test.ts › partial refund rounds to cents
  Expected 12.35, received 12.349999
  Fix the failures above, then finish. Do not weaken or skip tests.
claude: The rounding is off. Fixing src/orders/refund.ts:42…
claude: All 48 tests pass and types are clean. Done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You didn't paste anything. Claude found out it wasn't done, and fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing a good check
&lt;/h2&gt;

&lt;p&gt;The hook is only as good as the command in &lt;code&gt;verify.txt&lt;/code&gt;. A good one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exits non-zero on failure.&lt;/strong&gt; Every normal test runner already does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs without prompts.&lt;/strong&gt; No watch mode, no "press q to quit". &lt;code&gt;vitest&lt;/code&gt; needs &lt;code&gt;vitest run&lt;/code&gt;, and &lt;code&gt;jest&lt;/code&gt; needs &lt;code&gt;--watchAll=false&lt;/code&gt; in some setups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is fast enough to run on every stop.&lt;/strong&gt; If your full suite takes 10 minutes, point it at the part you're working on: &lt;code&gt;pytest tests/billing -q&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actually covers the feature.&lt;/strong&gt; Typecheck plus the relevant tests beats either alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
pytest &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ruff check &lt;span class="nb"&gt;.&lt;/span&gt;
go &lt;span class="nb"&gt;test&lt;/span&gt; ./... &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; go vet ./...
cargo &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo clippy &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; warnings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows gotcha:&lt;/strong&gt; the command runs through &lt;code&gt;cmd.exe&lt;/code&gt;, which doesn't treat single quotes as quotes. &lt;code&gt;python -c 'exit(1)'&lt;/code&gt; silently &lt;em&gt;passes&lt;/em&gt; there, because Python evaluates a string literal and exits 0. Use double quotes inside &lt;code&gt;verify.txt&lt;/code&gt;. I found this out when my own README example "passed" a test that should have failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 20-line version falls short
&lt;/h2&gt;

&lt;p&gt;It works, but a real session exposes some edge cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It runs even when nothing changed.&lt;/strong&gt; Ask Claude a question, and your whole suite runs before it can answer. Better: skip when &lt;code&gt;git status&lt;/code&gt; is clean, or when the tree hasn't changed since the last passing run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hanging command wedges the turn.&lt;/strong&gt; &lt;code&gt;timeout=300&lt;/code&gt; raises an exception but can leave child processes running (a dev server, a test watcher). Better: kill the whole process tree, and report the timeout as a failure with advice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;consecutive_blocks&lt;/code&gt; may not always be there.&lt;/strong&gt; Keep your own per-session counter as a backup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crashes should never break a session.&lt;/strong&gt; Any unexpected error should exit 0 and let Claude stop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I handled all four in a stdlib-only version that works on Windows, macOS and Linux. It's free and MIT licensed: &lt;strong&gt;&lt;a href="https://github.com/elijahmanlockedin112/verify-gate-hook" rel="noopener noreferrer"&gt;verify-gate-hook on GitHub&lt;/a&gt;&lt;/strong&gt;. Setup is the same three steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two habits that make it work better
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write the check before the code.&lt;/strong&gt; If you write &lt;code&gt;verify.txt&lt;/code&gt; first and run it once, it should &lt;em&gt;fail&lt;/em&gt;, because the feature doesn't exist yet. If it passes, it isn't testing the feature. Tighten it before you start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch for weakened tests.&lt;/strong&gt; The hook's message tells Claude not to weaken or skip tests, and it usually listens. Still skim the diff for edited assertions. A gate is only as honest as the test it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The gate answers "is it done?" The harder part is deciding what "done" means before Claude starts. I ended up building a full Claude Code setup around this loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/lock-in&lt;/code&gt; has Claude ask you the hard questions, then write a spec with a single verification command. It writes that command to &lt;code&gt;verify.txt&lt;/code&gt;, checks that it fails first, builds step by step against it, and finally has a fresh subagent review the diff against the spec.&lt;/li&gt;
&lt;li&gt;Hooks block &lt;code&gt;rm -rf ~&lt;/code&gt;, force-pushes to &lt;code&gt;main&lt;/code&gt;, and reading &lt;code&gt;.env&lt;/code&gt; before those commands ever run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/handoff&lt;/code&gt; → &lt;code&gt;/clear&lt;/code&gt; → &lt;code&gt;/pickup&lt;/code&gt; saves your progress to a file and resumes from it in a fresh session, so your context stays small and your usage limit lasts longer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the &lt;a href="https://lockedin.clarionproductlabs.com/" rel="noopener noreferrer"&gt;Locked In Kit&lt;/a&gt;: 18 skills, 6 subagents, 5 hooks, 8 CLAUDE.md templates and a playbook. The first 20 people get 30% off with code &lt;code&gt;LAUNCH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The hook above is free and works fine on its own, though. Add it to one project today and see how often Claude "finishes" with failing tests.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Independent project, not affiliated with Anthropic. Hook behavior is from the &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Claude Code hooks reference&lt;/a&gt;. If something behaves differently in your version, trust the docs.&lt;/em&gt;&lt;/p&gt;

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