<?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: Ian</title>
    <description>The latest articles on DEV Community by Ian (@ianymu).</description>
    <link>https://dev.to/ianymu</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3816899%2Ff9df278a-c359-4415-951b-01073e2cef1e.jpg</url>
      <title>DEV Community: Ian</title>
      <link>https://dev.to/ianymu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ianymu"/>
    <language>en</language>
    <item>
      <title>How 3 Claude Code Hook Strategies Compare for Preventing False-Completion</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Wed, 20 May 2026 16:30:07 +0000</pubDate>
      <link>https://dev.to/ianymu/how-3-claude-code-hook-strategies-compare-for-preventing-false-completion-d7m</link>
      <guid>https://dev.to/ianymu/how-3-claude-code-hook-strategies-compare-for-preventing-false-completion-d7m</guid>
      <description>&lt;p&gt;You ask Claude Code to add unit tests for the auth module. It works for two minutes and replies: &lt;em&gt;"I've added comprehensive tests and verified they all pass."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You run &lt;code&gt;git diff&lt;/code&gt;. There are three new test files. You run &lt;code&gt;npm test&lt;/code&gt;. The output is &lt;code&gt;0 tests ran&lt;/code&gt;. The files exist. They contain no actual &lt;code&gt;test()&lt;/code&gt; or &lt;code&gt;it()&lt;/code&gt; calls — just stubs and TODO comments.&lt;/p&gt;

&lt;p&gt;This is not a hallucination in the strict sense. The model wrote &lt;em&gt;something&lt;/em&gt;. It just bundled an unearned closeout phrase onto the end of a half-finished task. Cemri et al. (NeurIPS 2025, "Multi-Agent System failure Taxonomy", &lt;a href="https://arxiv.org/abs/2503.13657" rel="noopener noreferrer"&gt;arXiv:2503.13657&lt;/a&gt;) call this &lt;strong&gt;Mode 3.3 — No or Incorrect Verification&lt;/strong&gt;, and in their corpus of 1600+ annotated multi-agent traces it accounts for the single largest slice inside the "task verification &amp;amp; termination" category (21.3% of all failures; Mode 3.3 dominates that category). The MAD dataset they published makes the pattern empirically reproducible.&lt;/p&gt;

&lt;p&gt;You cannot prompt your way out of this. "Please verify before claiming done" is in roughly every CLAUDE.md on GitHub, and the trace data says it does not work as a steady-state mitigation. The intervention that actually moves the needle is &lt;strong&gt;out-of-band&lt;/strong&gt; — a Claude Code hook that runs deterministic code at the session boundary and refuses to let the closeout through.&lt;/p&gt;

&lt;p&gt;This post compares three production-grade approaches: &lt;strong&gt;&lt;code&gt;verify-before-stop&lt;/code&gt;&lt;/strong&gt; (log-based contract), &lt;strong&gt;&lt;code&gt;no-vibes&lt;/code&gt;&lt;/strong&gt; (text-vocabulary judge), and &lt;strong&gt;&lt;code&gt;no-unreachable-symbol&lt;/code&gt;&lt;/strong&gt; (codebase-static-analysis advisor). They are not competing — they catch different sub-failures of the same Mode 3.3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 1: &lt;code&gt;verify-before-stop&lt;/code&gt; — log-based contract
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ianymu/claude-verify-before-stop" rel="noopener noreferrer"&gt;&lt;code&gt;ianymu/claude-verify-before-stop&lt;/code&gt;&lt;/a&gt; treats verification as a &lt;strong&gt;discrete event that must be recorded&lt;/strong&gt;. The hook fires on Claude Code's &lt;code&gt;Stop&lt;/code&gt; event, reads the working-tree diff, and refuses to allow session-end if files changed but no fresh &lt;code&gt;VERIFIED&lt;/code&gt; log entry exists.&lt;/p&gt;

&lt;p&gt;The mechanism is a contract, not a heuristic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# .claude/hooks/verify-before-stop.sh (excerpt)&lt;/span&gt;

&lt;span class="c"&gt;# Read Stop-event payload from stdin&lt;/span&gt;
&lt;span class="nv"&gt;PAYLOAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PAYLOAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.stop_hook_active // false'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="c"&gt;# Did files change this session?&lt;/span&gt;
&lt;span class="nv"&gt;CHANGED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; HEAD 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git ls-files &lt;span class="nt"&gt;--others&lt;/span&gt; &lt;span class="nt"&gt;--exclude-standard&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="c"&gt;# Require a VERIFIED entry written within the last 5 minutes&lt;/span&gt;
&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".claude/state/stop-verify.log"&lt;/span&gt;
&lt;span class="nv"&gt;CUTOFF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;LATEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'|VERIFIED'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LATEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LATEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CUTOFF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BLOCKED: files changed but no VERIFIED entry in last 5 min."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Run your verification (npm test / curl / psql) then:"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  echo &lt;/span&gt;&lt;span class="se"&gt;\"\$&lt;/span&gt;&lt;span class="s2"&gt;(date +%s)|VERIFIED&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2  &lt;span class="c"&gt;# exit 2 = block stop, surface stderr to model&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the session, Claude is expected to log evidence explicitly:&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
echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;|VERIFY_ACTION|npm test passed"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .claude/state/stop-verify.log
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;|VERIFIED"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .claude/state/stop-verify.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it catches well&lt;/strong&gt;: the entire class of failures where the model writes a confident closing message without ever shelling out to a verifier. Because the contract is &lt;em&gt;external&lt;/em&gt; (the log file is outside the model's context window), paraphrase attacks don't work — there is no clever phrasing that satisfies a missing log entry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does not catch&lt;/strong&gt;: a model that &lt;em&gt;does&lt;/em&gt; write a &lt;code&gt;VERIFIED&lt;/code&gt; line but whose verification was bogus (&lt;code&gt;echo "VERIFIED"&lt;/code&gt; with no prior test command). It enforces &lt;em&gt;that&lt;/em&gt; verification happened, not &lt;em&gt;that the verification was correct&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs&lt;/strong&gt;: zero language coverage problem (it doesn't read code or text — it reads filesystem state), zero false-positive rate by construction (silence on pure-conversation turns where no files changed), 60-second setup, MIT-licensed, no dependencies beyond &lt;code&gt;bash&lt;/code&gt; + &lt;code&gt;python3&lt;/code&gt; stdlib.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 2: &lt;code&gt;no-vibes&lt;/code&gt; — text-vocabulary judge
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/waitdeadai/no-vibes" rel="noopener noreferrer"&gt;&lt;code&gt;waitdeadai/no-vibes&lt;/code&gt;&lt;/a&gt;, part of the &lt;a href="https://github.com/waitdeadai/llm-dark-patterns" rel="noopener noreferrer"&gt;&lt;code&gt;llm-dark-patterns&lt;/code&gt;&lt;/a&gt; suite, takes the opposite angle. It reads the &lt;em&gt;model's outgoing text&lt;/em&gt; on &lt;code&gt;Stop&lt;/code&gt; and looks for the linguistic signature of unearned closeouts — "all tests pass", "looks good", "should work", "verified" — when no proximate evidence (a fenced block with tool output, a recognized verifier binary name, a hash, an exit code) appears in the same message.&lt;/p&gt;

&lt;p&gt;The detector is deterministic regex + locale packs + an evidence-binary allowlist with 200+ entries spanning app-dev, devops, k8s, cloud, and database tooling. Operators extend it without forking by dropping a &lt;code&gt;.txt&lt;/code&gt; file at &lt;code&gt;${XDG_CONFIG_HOME}/llm-dark-patterns/packs/&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Conceptual pattern (the real hook is ~530 lines with negation handling and proximity windows):&lt;/span&gt;

&lt;span class="nv"&gt;POSITIVE_CLOSEOUT_RE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\b(all tests pass(ed|ing)?|looks good|should work|verified|fixed|done|complete[d]?)\b'&lt;/span&gt;
&lt;span class="nv"&gt;EVIDENCE_BINARY_RE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\b(npm|pytest|cargo|go test|jest|playwright|curl|psql|kubectl|terraform|...)\b'&lt;/span&gt;

&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.message.content // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSITIVE_CLOSEOUT_RE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EVIDENCE_BINARY_RE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no-vibes: positive closeout without same-message evidence."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Either run the verifier and quote its output, or hedge the claim."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;2
  &lt;span class="k"&gt;fi
fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Empirical baseline&lt;/strong&gt;: F1 &lt;strong&gt;0.815&lt;/strong&gt; (95% CI [0.615, 0.941], n=19) on the human-labelled subset of MAD against Mode 3.3 — published at the umbrella suite's &lt;a href="https://github.com/waitdeadai/llm-dark-patterns/blob/main/evaluation/MAST-RESULTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;evaluation/MAST-RESULTS.md&lt;/code&gt;&lt;/a&gt;. On the full LLM-judge subset (n=954) it scores F1 0.308 — recall is high (0.486) and precision falls because the LLM judge is noisier than human annotators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it catches well&lt;/strong&gt;: the "confidence theater" surface. Cases where the model has the &lt;em&gt;vocabulary&lt;/em&gt; of a successful turn but didn't actually run anything. Useful as a pure complement to &lt;code&gt;verify-before-stop&lt;/code&gt; because it can fire mid-message (PreToolUse / Stop) rather than only at session-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does not catch&lt;/strong&gt;: a model that closes flatly — no positive verbs at all, just "Here are the files I added." Some failure modes route around &lt;code&gt;no-vibes&lt;/code&gt; by producing prose that is technically not a positive closeout but still implies completion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs&lt;/strong&gt;: language coverage is per-locale pack (English, Spanish, Polish ship; others need a &lt;code&gt;.txt&lt;/code&gt; contribution). False-positive rate is non-zero — operators report occasional fires on legitimate hedged-positive turns, hence the clause-local negation hardening in the May 2026 release. Setup: 30 seconds via the plugin marketplace or one curl.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 3: &lt;code&gt;no-unreachable-symbol&lt;/code&gt; — codebase static-analysis advisor
&lt;/h2&gt;

&lt;p&gt;The third strategy lives at a different boundary entirely. &lt;a href="https://github.com/waitdeadai/llm-dark-patterns/blob/main/hooks/no-unreachable-symbol.sh" rel="noopener noreferrer"&gt;&lt;code&gt;no-unreachable-symbol&lt;/code&gt;&lt;/a&gt; fires on &lt;code&gt;Stop&lt;/code&gt;, diffs the working tree against &lt;code&gt;HEAD&lt;/code&gt;, extracts new public Python symbols from added lines, and flags symbols with zero callers under an exclusion-aware grep that understands decorators (&lt;code&gt;@app.route&lt;/code&gt;, &lt;code&gt;@pytest.fixture&lt;/code&gt;, &lt;code&gt;@click.command&lt;/code&gt;), &lt;code&gt;__all__&lt;/code&gt; markers, registry patterns (&lt;code&gt;HANDLERS["foo"] = sym&lt;/code&gt;), and private prefixes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Conceptual flow (the real hook is ~200 lines):&lt;/span&gt;

&lt;span class="nv"&gt;DIFF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff HEAD &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*.py'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;NEW_SYMBOLS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIFF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^\+\s*(def|class)\s+'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s/^\+\s*(def|class)\s+([a-zA-Z_][a-zA-Z0-9_]*).*/\2/'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Skip private (_foo) and dunder (__foo__) symbols&lt;/span&gt;
&lt;span class="c"&gt;# Skip decorator-wired symbols (framework callbacks)&lt;/span&gt;
&lt;span class="c"&gt;# Respect __all__ public-API markers&lt;/span&gt;
&lt;span class="c"&gt;# ... (full exclusion logic in the real script)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;sym &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$REMAINING_SYMBOLS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;CALLERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="nv"&gt;$sym&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.py'&lt;/span&gt; &lt;span class="nt"&gt;--exclude-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tests &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vE&lt;/span&gt; &lt;span class="s2"&gt;"^[^:]+:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*(def|class)&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="nv"&gt;$sym&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CALLERS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ADVISORY: new public symbol &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="nv"&gt;$sym&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt; has zero callers."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="k"&gt;fi
done&lt;/span&gt;

&lt;span class="c"&gt;# Default: advisory mode (exit 0 with stderr). Strict mode via env:&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LDP_UNREACHABLE_SYMBOL_BLOCK&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it catches well&lt;/strong&gt;: the specific failure where Claude generates a new public function or class as part of a refactor, claims the refactor is wired up, but never edits any caller. This is invisible to &lt;code&gt;no-vibes&lt;/code&gt; (no positive-closeout language was used) and invisible to &lt;code&gt;verify-before-stop&lt;/code&gt; (the model dutifully logged &lt;code&gt;VERIFIED&lt;/code&gt; after running tests — which passed because nothing called the new symbol).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does not catch&lt;/strong&gt;: non-Python languages (Slice 0 is Python-only; TS/JS/Rust/Go are roadmap), and dynamic-dispatch patterns where the symbol &lt;em&gt;is&lt;/em&gt; called but via &lt;code&gt;getattr&lt;/code&gt; / string-based reflection that grep cannot resolve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs&lt;/strong&gt;: language coverage is limited (Python at Slice 0). False-positive rate is non-zero for codebases that rely heavily on registry patterns the hook doesn't recognize — hence the advisory-by-default ship mode, with strict-blocking opt-in via &lt;code&gt;LDP_UNREACHABLE_SYMBOL_BLOCK=1&lt;/code&gt;. No empirical F1 baseline because MAD is text-only traces with no git-diff-vs-codebase ground truth; the hook ships against a &lt;a href="https://github.com/waitdeadai/llm-dark-patterns/blob/main/tests/no-unreachable-symbol/smoke.sh" rel="noopener noreferrer"&gt;12-scenario smoke harness&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;&lt;code&gt;verify-before-stop&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;no-vibes&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;no-unreachable-symbol&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Signal source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;filesystem (&lt;code&gt;VERIFIED&lt;/code&gt; log)&lt;/td&gt;
&lt;td&gt;model's outgoing text&lt;/td&gt;
&lt;td&gt;git diff + codebase grep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operator effort per session&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;active (must &lt;code&gt;echo VERIFIED&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;passive&lt;/td&gt;
&lt;td&gt;passive (advisory) / active opt-in (strict)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Catches false closeout phrasing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no (out-of-band)&lt;/td&gt;
&lt;td&gt;yes (primary target)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Catches verified-but-bogus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;partially&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Catches dead-code-on-merge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;language-agnostic&lt;/td&gt;
&lt;td&gt;per-locale pack (en/es/pl ship)&lt;/td&gt;
&lt;td&gt;Python (Slice 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Empirical F1 vs MAST 3.3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not published (strict contract; no false-negative-acceptable mode)&lt;/td&gt;
&lt;td&gt;0.815 human-labelled, 0.308 LLM-judge&lt;/td&gt;
&lt;td&gt;n/a (no ground truth dataset)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;False-positive rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~0 (silence when no files changed)&lt;/td&gt;
&lt;td&gt;low but non-zero (hedge-then-positive cases)&lt;/td&gt;
&lt;td&gt;non-zero on heavy-registry codebases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;60s&lt;/td&gt;
&lt;td&gt;30s (plugin marketplace) or 60s (curl)&lt;/td&gt;
&lt;td&gt;30s (part of umbrella plugin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;verify-before-stop&lt;/code&gt; if&lt;/strong&gt; you are running a single-developer Claude Code project with frequent destructive operations (database migrations, API deploys, infra changes) where the cost of an unverified closeout is high (rollback, on-call page, data loss). The active-logging discipline is friction &lt;em&gt;by design&lt;/em&gt; — it forces a verification step into muscle memory. Best fit for shipping individual contributors who have been bitten by Mode 3.3 in production and want the hardest contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;no-vibes&lt;/code&gt; if&lt;/strong&gt; you are working across many short turns where active logging is impractical (e.g. extended exploratory sessions, planning work, multi-agent supervisor closeouts). It is also the right pick for multi-language stacks because the locale packs and evidence-binary allowlist generalize, and for teams where the &lt;em&gt;vocabulary&lt;/em&gt; of false success is the primary failure surface — overconfident closeouts from junior-coded prompts or model drift on long sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;no-unreachable-symbol&lt;/code&gt; if&lt;/strong&gt; your codebase is primarily Python and your dominant failure shape is the "refactor mirage" — Claude added the new helper but never edited any caller, tests still pass because nothing exercises the new symbol, and you only discover it when reviewing the PR. Especially useful for library work where unused public symbols become part of the API surface and accrue maintenance cost forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision shortcuts&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;"My tests are green but my prod is on fire"&lt;/em&gt; → &lt;code&gt;verify-before-stop&lt;/code&gt; (you have a test/reality mismatch, not a model-vocabulary problem).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Claude says 'looks good' on turns where I know it didn't run anything"&lt;/em&gt; → &lt;code&gt;no-vibes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"My PR reviewers keep finding orphan code Claude added"&lt;/em&gt; → &lt;code&gt;no-unreachable-symbol&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Combining all three: defense-in-depth
&lt;/h2&gt;

&lt;p&gt;The hooks compose. Each closes a different gap. A session that survives all three has had filesystem-contract, text-evidence, and symbol-evidence line up — which is roughly the contract MAST 3.3 actually asks for.&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;"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;"*"&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;"bash .claude/hooks/verify-before-stop.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="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;"bash .claude/hooks/no-vibes.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="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;"bash .claude/hooks/no-unreachable-symbol.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"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;"*"&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;"bash .claude/hooks/no-vibes.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Order matters: &lt;code&gt;verify-before-stop&lt;/code&gt; first (cheapest check; exits early on read-only sessions), then &lt;code&gt;no-vibes&lt;/code&gt; (text scan), then &lt;code&gt;no-unreachable-symbol&lt;/code&gt; (most expensive — runs a full repo grep). Any hook returning &lt;code&gt;exit 2&lt;/code&gt; blocks the stop and surfaces its stderr to the model, which then has the corrective context on the next turn.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want a starting point tailored to your stack instead of a generic template, &lt;a href="https://landing-ianymu.vercel.app/audit/" rel="noopener noreferrer"&gt;Ian's free audit tool&lt;/a&gt; generates three personalized hook recommendations from your repo's language mix and CI setup in about 41 seconds — no signup, no email gate.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>anthropic</category>
    </item>
    <item>
      <title>Stop Claude Code from lying about completion — a 50-line bash hook</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Wed, 20 May 2026 11:38:04 +0000</pubDate>
      <link>https://dev.to/ianymu/stop-claude-code-from-lying-about-completion-a-50-line-bash-hook-1g2b</link>
      <guid>https://dev.to/ianymu/stop-claude-code-from-lying-about-completion-a-50-line-bash-hook-1g2b</guid>
      <description>&lt;p&gt;After 12 months running Claude Code on 14 parallel projects, the same regression cycle kept happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude: "All tests passing ✅"
Me:     [merges PR]
Prod:   [throws 500s]
Me:     [2h debugging]
Tomorrow: [same cycle]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model isn't lying on purpose — it's just optimistic about its own work. &lt;strong&gt;The fix isn't a better prompt. The fix is a workflow guard.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built &lt;code&gt;verify-before-stop.sh&lt;/code&gt; — a Stop hook that blocks the session from ending until verification is actually logged. Here's the full implementation and why it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Claude Code's &lt;code&gt;Stop&lt;/code&gt; hook fires when the model tries to end a session. The hook receives JSON on stdin with &lt;code&gt;stop_hook_active&lt;/code&gt;, &lt;code&gt;transcript_path&lt;/code&gt;, &lt;code&gt;session_id&lt;/code&gt;. If the hook exits non-zero with stderr message, Claude continues the turn — and the stderr becomes part of the model's context.&lt;/p&gt;

&lt;p&gt;That's the leverage point. &lt;strong&gt;If the model claims "done" without proof, exit 2 with instructions for what proof is required.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hook (50 lines)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# verify-before-stop.sh&lt;/span&gt;

&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Avoid infinite loop — Claude Code sets stop_hook_active=true&lt;/span&gt;
&lt;span class="c"&gt;# when continuing from a previous block&lt;/span&gt;
&lt;span class="nv"&gt;STOP_HOOK_ACTIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"import sys,json; d=json.load(sys.stdin); print('true' if d.get('stop_hook_active') else 'false')"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STOP_HOOK_ACTIVE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;VERIFY_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".claude/state/stop-verify.log"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .claude/state

&lt;span class="c"&gt;# No file changes → pure conversation → allow stop&lt;/span&gt;
&lt;span class="nv"&gt;HAS_CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;HAS_UNTRACKED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git ls-files &lt;span class="nt"&gt;--others&lt;/span&gt; &lt;span class="nt"&gt;--exclude-standard&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'.claude/state/'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HAS_CHANGES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HAS_UNTRACKED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Files changed → require VERIFIED log entry in last 5 min&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERIFY_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;FIVE_MIN_AGO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-v-5M&lt;/span&gt; +%s 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'5 minutes ago'&lt;/span&gt; +%s 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;LAST_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'|VERIFIED'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERIFY_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;LAST_ACTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'|VERIFY_ACTION'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERIFY_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LAST_VERIFY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LAST_VERIFY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FIVE_MIN_AGO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LAST_ACTION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LAST_ACTION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FIVE_MIN_AGO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
            &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;|STOP_ALLOWED"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERIFY_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
            &lt;span class="nb"&gt;exit &lt;/span&gt;0
        &lt;span class="k"&gt;fi
    fi
fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;|STOP_BLOCKED"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERIFY_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"⛔ BLOCKED: files changed but no verification logged in last 5 min."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Required: log a VERIFY_ACTION + VERIFIED entry, e.g.:"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'   echo "$(date +%s)|VERIFY_ACTION|ran npm test, 3 failures" &amp;gt;&amp;gt; .claude/state/stop-verify.log'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'   echo "$(date +%s)|VERIFIED" &amp;gt;&amp;gt; .claude/state/stop-verify.log'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;Drop into &lt;code&gt;.claude/hooks/&lt;/code&gt; and wire it up:&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;.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;"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="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;"*"&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;"bash .claude/hooks/verify-before-stop.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart your Claude Code session. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  How verification works in practice
&lt;/h2&gt;

&lt;p&gt;When Claude tries to end after edits, the hook blocks with stderr telling it exactly what to log. The model then has to either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# A) Actually verify&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;span class="c"&gt;# ...test output...&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;|VERIFY_ACTION|npm test all green"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .claude/state/stop-verify.log
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;|VERIFIED"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .claude/state/stop-verify.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# B) Admit it didn't verify&lt;/span&gt;
&lt;span class="c"&gt;# Claude in next turn: "I couldn't actually run the tests — they require a&lt;/span&gt;
&lt;span class="c"&gt;# dev DB. Here's what I changed; please run npm test manually before merging."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, you stop getting false "done" reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  4 design decisions worth defending
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Why a 5-minute TTL on &lt;code&gt;VERIFIED&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Long enough that legitimate verify→stop sequences don't trip on it. Short enough that stale entries from yesterday don't accidentally allow stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Why &lt;code&gt;git diff&lt;/code&gt; as the change signal?&lt;/strong&gt;&lt;br&gt;
Catches both edits and untracked new files. Excludes &lt;code&gt;.claude/state/&lt;/code&gt; (the hook's own writes) to avoid self-triggering loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Why exit 2 (not 1)?&lt;/strong&gt;&lt;br&gt;
Claude Code treats exit 2 as a Stop block with stderr passed to the model. Exit 1 is treated as a generic error and may not surface the message correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Why explicit &lt;code&gt;VERIFY_ACTION&lt;/code&gt; + &lt;code&gt;VERIFIED&lt;/code&gt; two-line pattern?&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;VERIFY_ACTION&lt;/code&gt; line forces the model to commit &lt;em&gt;what it verified&lt;/em&gt; in writing — not just claim it. Doubles as an audit trail you can grep later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch out for the cap
&lt;/h2&gt;

&lt;p&gt;Claude Code v2.1.143+ added a built-in safeguard: after ~8 consecutive Stop-hook blocks, the turn ends with a warning regardless. This is intentional — prevents broken hooks from infinite-looping the model. Override with &lt;code&gt;CLAUDE_CODE_STOP_HOOK_BLOCK_CAP=20&lt;/code&gt; if you have legitimate reason for more retries.&lt;/p&gt;

&lt;p&gt;Design your hook to give the model &lt;em&gt;enough information to comply on the next turn&lt;/em&gt;, not on the 8th turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this saved me
&lt;/h2&gt;

&lt;p&gt;After 6 months of using this hook across 14 projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eliminated&lt;/strong&gt; "AI says tests pass, they didn't" regressions — the #1 source of "wait, I thought you said this worked" merges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forced&lt;/strong&gt; explicit verification logging — &lt;code&gt;.claude/state/stop-verify.log&lt;/code&gt; now reads like an audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Survived&lt;/strong&gt; conversation compaction — the log file persists, so even after Claude Code summarizes the session, the verification record is still on disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open source
&lt;/h2&gt;

&lt;p&gt;Full source on GitHub: &lt;a href="https://github.com/ianymu/claude-verify-before-stop" rel="noopener noreferrer"&gt;https://github.com/ianymu/claude-verify-before-stop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT license. Star it if you find it useful — easier to find when you need it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want the rest
&lt;/h2&gt;

&lt;p&gt;I packaged this with 5 more hooks I built over those 12 months — &lt;code&gt;cost-tracker.sh&lt;/code&gt; (logs every $ of Opus spend to &lt;code&gt;costs.jsonl&lt;/code&gt;), &lt;code&gt;block-secrets.sh&lt;/code&gt; (scans Write/Edit/Bash for &lt;code&gt;sk-ant-*&lt;/code&gt;/JWT/AWS keys before commit), &lt;code&gt;force-progress-update.sh&lt;/code&gt; (checkpoint every 5 actions to survive compaction), &lt;code&gt;pre-compact-diary.sh&lt;/code&gt; (preserves WIP context), &lt;code&gt;enforce-autoplan.sh&lt;/code&gt; (no code-write without a plan).&lt;/p&gt;

&lt;p&gt;Full 6-pack is &lt;strong&gt;$49 launch price&lt;/strong&gt; (regular $79), 30-day money-back, instant download via Polar: &lt;a href="https://landing-ianymu.vercel.app" rel="noopener noreferrer"&gt;https://landing-ianymu.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But honestly — &lt;code&gt;verify-before-stop&lt;/code&gt; alone solves 80% of the pain. Start there. Use it free for a week, then decide if the rest is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;Curious if you've built similar workflow guards. The verify-before-stop pattern feels obvious in hindsight — what other "lies of completion" patterns have you caught?&lt;/p&gt;

&lt;p&gt;Drop your own hooks in the comments. If they're solid I'll add them to the README with credit.&lt;/p&gt;

&lt;p&gt;— Ian&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I analyzed thousands of founder posts and built a platform to solve the #1 complaint</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Tue, 10 Mar 2026 12:44:42 +0000</pubDate>
      <link>https://dev.to/yanlong_mu_f5167490b87724/i-analyzed-thousands-of-founder-posts-and-built-a-platform-to-solve-the-1-complaint-dab</link>
      <guid>https://dev.to/yanlong_mu_f5167490b87724/i-analyzed-thousands-of-founder-posts-and-built-a-platform-to-solve-the-1-complaint-dab</guid>
      <description>&lt;p&gt;6 months ago I quit my job to build products solo. The coding was fine. Marketing I could figure out. What almost made me give up was the silence.&lt;/p&gt;

&lt;p&gt;Not "I'm lonely" silence. More like — I just spent three weeks on a feature and I genuinely can't tell if it's brilliant or stupid. And there's nobody to ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  The research
&lt;/h2&gt;

&lt;p&gt;I started reading everything solo founders were posting online. Reddit, HN, IndieHackers, Twitter. Thousands of posts. Same three problems everywhere:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No accountability&lt;/strong&gt;&lt;br&gt;
You set goals on Monday. By Wednesday they're gone. Nobody notices, nobody cares. Without a co-founder or team, goals are just wishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generic advice&lt;/strong&gt;&lt;br&gt;
"Have you tried content marketing?" Thanks. Very helpful. The advice you get from people who don't know your product is almost always useless. Not because they don't care — they just don't have context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Communities built for broadcasting, not connection&lt;/strong&gt;&lt;br&gt;
You scroll, read, maybe post something. It gets 2 upvotes. You move on. There are hundreds of founder communities, but they're all optimized for content consumption, not for actual relationships.&lt;/p&gt;

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

&lt;p&gt;The one thing that changed my productivity was finding another founder at my exact stage. We did weekly 30-minute calls. Simple format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What did you do this week?&lt;/li&gt;
&lt;li&gt;What's the plan for next week?&lt;/li&gt;
&lt;li&gt;What's blocking you?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My output doubled. Just knowing someone would ask me on Friday whether I did the thing I said I'd do on Monday — that was enough.&lt;/p&gt;

&lt;p&gt;Then he got busy, the calls stopped, and I went back to building alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building
&lt;/h2&gt;

&lt;p&gt;So I'm making a platform called &lt;strong&gt;ADot&lt;/strong&gt; to solve this at scale.&lt;/p&gt;

&lt;p&gt;The core: AI matches you with a founder at your stage (pre-launch? first 10 users? trying to hit $1K MRR?) for structured weekly check-ins. Not networking events. Not another Slack group. Actual accountability with someone who gets it.&lt;/p&gt;

&lt;p&gt;On top of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Community feed where milestones get seen (not buried)&lt;/li&gt;
&lt;li&gt;AI recommendations based on what similar products did to grow&lt;/li&gt;
&lt;li&gt;Open-source founder tools (pain point analyzer, competitor tracker)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;Just launched the landing page. Validating whether this resonates or if I'm projecting.&lt;/p&gt;

&lt;p&gt;If you've ever built alone and wished someone was in the trenches with you: &lt;a href="https://adot-lp.vercel.app?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=launch_v1" rel="noopener noreferrer"&gt;https://adot-lp.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback &amp;gt;&amp;gt; signups. Tell me why this won't work.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>buildinpublic</category>
      <category>sideprojects</category>
      <category>solopreneur</category>
    </item>
    <item>
      <title>I analyzed thousands of founder posts and built a platform to solve the #1 complaint</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Tue, 10 Mar 2026 12:44:42 +0000</pubDate>
      <link>https://dev.to/yanlong_mu_f5167490b87724/i-analyzed-thousands-of-founder-posts-and-built-a-platform-to-solve-the-1-complaint-bch</link>
      <guid>https://dev.to/yanlong_mu_f5167490b87724/i-analyzed-thousands-of-founder-posts-and-built-a-platform-to-solve-the-1-complaint-bch</guid>
      <description>&lt;p&gt;6 months ago I quit my job to build products solo. The coding was fine. Marketing I could figure out. What almost made me give up was the silence.&lt;/p&gt;

&lt;p&gt;Not "I'm lonely" silence. More like — I just spent three weeks on a feature and I genuinely can't tell if it's brilliant or stupid. And there's nobody to ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  The research
&lt;/h2&gt;

&lt;p&gt;I started reading everything solo founders were posting online. Reddit, HN, IndieHackers, Twitter. Thousands of posts. Same three problems everywhere:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No accountability&lt;/strong&gt;&lt;br&gt;
You set goals on Monday. By Wednesday they're gone. Nobody notices, nobody cares. Without a co-founder or team, goals are just wishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generic advice&lt;/strong&gt;&lt;br&gt;
"Have you tried content marketing?" Thanks. Very helpful. The advice you get from people who don't know your product is almost always useless. Not because they don't care — they just don't have context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Communities built for broadcasting, not connection&lt;/strong&gt;&lt;br&gt;
You scroll, read, maybe post something. It gets 2 upvotes. You move on. There are hundreds of founder communities, but they're all optimized for content consumption, not for actual relationships.&lt;/p&gt;

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

&lt;p&gt;The one thing that changed my productivity was finding another founder at my exact stage. We did weekly 30-minute calls. Simple format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What did you do this week?&lt;/li&gt;
&lt;li&gt;What's the plan for next week?&lt;/li&gt;
&lt;li&gt;What's blocking you?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My output doubled. Just knowing someone would ask me on Friday whether I did the thing I said I'd do on Monday — that was enough.&lt;/p&gt;

&lt;p&gt;Then he got busy, the calls stopped, and I went back to building alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building
&lt;/h2&gt;

&lt;p&gt;So I'm making a platform called &lt;strong&gt;ADot&lt;/strong&gt; to solve this at scale.&lt;/p&gt;

&lt;p&gt;The core: AI matches you with a founder at your stage (pre-launch? first 10 users? trying to hit $1K MRR?) for structured weekly check-ins. Not networking events. Not another Slack group. Actual accountability with someone who gets it.&lt;/p&gt;

&lt;p&gt;On top of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Community feed where milestones get seen (not buried)&lt;/li&gt;
&lt;li&gt;AI recommendations based on what similar products did to grow&lt;/li&gt;
&lt;li&gt;Open-source founder tools (pain point analyzer, competitor tracker)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;Just launched the landing page. Validating whether this resonates or if I'm projecting.&lt;/p&gt;

&lt;p&gt;If you've ever built alone and wished someone was in the trenches with you: &lt;a href="https://adot-lp.vercel.app?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=launch_v1" rel="noopener noreferrer"&gt;https://adot-lp.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback &amp;gt;&amp;gt; signups. Tell me why this won't work.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>buildinpublic</category>
      <category>sideprojects</category>
      <category>solopreneur</category>
    </item>
  </channel>
</rss>
