<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Eli Karpinsky</title><description>Eli Karpinsky&apos;s homepage: his work, his writing, and how to reach him.</description><link>https://karpinsky.io</link><item><title>Put the rule in a hook, not the prompt</title><link>https://karpinsky.io/writing/rule-in-a-hook</link><guid>https://karpinsky.io/writing/rule-in-a-hook</guid><pubDate>Thu, 01 Oct 2026 00:00:00 GMT</pubDate><description>Prompts carry judgment, hooks carry invariants: three small Claude Code hooks that stopped my agents 327 times in September, and where classifiers fit next.</description><content:encoded>&lt;p&gt;In September, three small hooks stopped my coding agents 327 times. Each stop was a rule I&apos;d already written down in a prompt.&lt;/p&gt;
&lt;p&gt;That&apos;s the split I use now. Prompts carry judgment. Hooks carry invariants. If a rule has to hold every time, it becomes a hook: a short script that runs on every tool call or turn end, and that the agent can&apos;t skip.&lt;/p&gt;
&lt;p&gt;I run 22 enforcement hooks in my Claude Code settings. This post walks through three of them, then where I&apos;m taking hooks next.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table&quot;&gt;&lt;table&gt;&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;What it enforces&lt;/th&gt;
&lt;th&gt;Blocks, Sept 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block-spin-wait&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No shell loops that sleep while waiting on something&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block-unscoped-tests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No full test suite runs locally&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;require-outstanding-sweep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No ending a turn with work still open&lt;/td&gt;
&lt;td&gt;158&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;&lt;h2 id=&quot;what-a-hook-is&quot;&gt;What a hook is &lt;a class=&quot;anchor&quot; href=&quot;#what-a-hook-is&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Claude Code runs a command of your choosing at fixed points in a session: before a tool call (&lt;code&gt;PreToolUse&lt;/code&gt;), when the agent tries to end its turn (&lt;code&gt;Stop&lt;/code&gt;), and about 30 others. The hook gets the event as JSON on stdin. Exit 0 lets it through. Exit 2 blocks it, and whatever the hook wrote to stderr goes back to the agent as the reason.&lt;/p&gt;
&lt;p&gt;So the rule isn&apos;t text the model weighs against everything else in its context. It&apos;s a check that runs every time, whatever the model thinks.&lt;/p&gt;
&lt;p&gt;Here&apos;s the core of &lt;code&gt;block-spin-wait&lt;/code&gt;:&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;header&gt;&lt;span&gt;js&lt;/span&gt;&lt;button type=&quot;button&quot; hidden&gt;Copy&lt;/button&gt;&lt;/header&gt;&lt;pre tabindex=&quot;0&quot; class=&quot;shiki karpinsky&quot; style=&quot;background-color:#030712;color:#e5e7eb&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; LOOP &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;until&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;\s&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;\w&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;\s&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;foreach&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; SLEEP &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;sleep&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;start-sleep&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt; main&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; payload &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt; readPayload&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; (!&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; process&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;exit&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#BFE3B0&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; tool &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; payload&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;tool_name &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;tool &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;Bash&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; tool &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;PowerShell&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; process&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;exit&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#BFE3B0&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; command &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;tool_input &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; payload&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;tool_input&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;command&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; (!&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;command&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; process&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;exit&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#BFE3B0&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; (!(&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;LOOP&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;test&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;command&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; SLEEP&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;test&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;command&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)))&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; process&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;exit&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#BFE3B0&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;  process&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;stderr&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;write&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;message&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;  process&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;exit&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#BFE3B0&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;&lt;p&gt;Two regexes and an exit code. Most of my hooks take one of two shapes: block an action, or gate the end of a turn. There&apos;s a third shape, requiring a step before an action, that I&apos;ll leave for another post.&lt;/p&gt;
&lt;h2 id=&quot;shape-1%3A-block-the-action&quot;&gt;Shape 1: block the action &lt;a class=&quot;anchor&quot; href=&quot;#shape-1%3A-block-the-action&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A block hook stops one specific action before it runs. I use it when I can recognize the bad action from the command alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;block-spin-wait&lt;/code&gt;.&lt;/strong&gt; A shell loop that sleeps (&lt;code&gt;until grep ...; do sleep 10; done&lt;/code&gt;) burns the turn watching for a state change instead of being woken by it. The hook blocks any loop that contains a sleep and points the agent at the right tool: run the long command in the background and stop, or use a monitor that fires one notification per event. A single &lt;code&gt;sleep 5&lt;/code&gt; with no loop around it still goes through. It blocked 86 times across 20 sessions in September.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;block-unscoped-tests&lt;/code&gt;.&lt;/strong&gt; A test command with no scope runs the whole suite locally. That&apos;s slow, and it&apos;s CI&apos;s job. The hook requires a file, a directory, or a &lt;code&gt;-t&lt;/code&gt; name pattern. It also catches a quieter case: &lt;code&gt;pnpm run test -- &amp;lt;path&amp;gt;&lt;/code&gt;, where the &lt;code&gt;--&lt;/code&gt; passes the path through in a way that drops the filter, so the &amp;quot;scoped&amp;quot; run is really the full suite. It blocked 83 times across 13 sessions.&lt;/p&gt;
&lt;p&gt;Here&apos;s what each one lets through, taken from their test cases:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table&quot;&gt;&lt;table&gt;&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Blocked&lt;/th&gt;
&lt;th&gt;Allowed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;until grep ...; do sleep 10; done&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sleep 5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;while ...; do sleep ...; done&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a plain command with no loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vitest run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vitest run &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vitest run -t &amp;quot;name&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pnpm run test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vitest run &amp;lt;directory&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pnpm run test -- &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;&lt;p&gt;The design point is in the reason text. A blocked command gets a message that says what to do instead, so a block costs the agent one retry, not a dead end. It reads the reason, switches to a background run or adds a scope, and keeps going.&lt;/p&gt;
&lt;h2 id=&quot;shape-2%3A-gate-the-stop&quot;&gt;Shape 2: gate the stop &lt;a class=&quot;anchor&quot; href=&quot;#shape-2%3A-gate-the-stop&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The most useful hook I have doesn&apos;t block a command. It blocks the agent from ending its turn when the turn isn&apos;t finished.&lt;/p&gt;
&lt;p&gt;&amp;quot;Finish the work before you stop&amp;quot; is exactly the kind of rule a model talks itself out of at the end of a long turn. It has done the main thing, the context is full, and wrapping up with &amp;quot;next I&apos;ll run the migration&amp;quot; or &amp;quot;want me to add tests?&amp;quot; feels reasonable. So I made that rule a &lt;code&gt;Stop&lt;/code&gt; hook.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;require-outstanding-sweep&lt;/code&gt;&lt;/strong&gt; reads the final message when the agent tries to stop. If it ends on a promise, an offer, or a disclaimer, the turn is refused and the agent is told to go do the work. The patterns are plain regexes:&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;header&gt;&lt;span&gt;js&lt;/span&gt;&lt;button type=&quot;button&quot; hidden&gt;Copy&lt;/button&gt;&lt;/header&gt;&lt;pre tabindex=&quot;0&quot; class=&quot;shiki karpinsky&quot; style=&quot;background-color:#030712;color:#e5e7eb&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; COMMITMENTS &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; re&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;I&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(?:&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&apos;m&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt; am&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; (?:&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;also &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)?&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;going to&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; label&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;I&apos;m going to …&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; re&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;I owe you&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; label&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;I owe you …&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; re&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;next &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(?:&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;\w&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; ){0,3}(?:&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;up&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;step&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;steps&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;move&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;thing&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; label&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;next up/is/step …&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;  // ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; OFFERS &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; re&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(?:&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;do you &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;would you &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)?&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;want me to&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; label&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;want me to …?&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; re&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;shall I&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; label&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;shall I …?&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; re&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;let me know&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;\b&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; label&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;let me know …&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;  // ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;];&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;&lt;p&gt;There are two ways out. The agent can say &lt;code&gt;NOTHING-OUTSTANDING&lt;/code&gt;, or it can say &lt;code&gt;BLOCKED-ON-USER[...]&lt;/code&gt; and carry proof:&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;header&gt;&lt;span&gt;js&lt;/span&gt;&lt;button type=&quot;button&quot; hidden&gt;Copy&lt;/button&gt;&lt;/header&gt;&lt;pre tabindex=&quot;0&quot; class=&quot;shiki karpinsky&quot; style=&quot;background-color:#030712;color:#e5e7eb&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; QUOTE &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;QUOTE:\s&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;([^&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;]{12,})&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; EVIDENCE &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;EVIDENCE:\s&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;([^&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;]{6,})&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; PHYSICAL &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;PHYSICAL:\s&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;?([^&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;\n&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;]{6,})&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;?/&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;&lt;p&gt;A &lt;code&gt;QUOTE&lt;/code&gt; has to match something the user actually wrote in this conversation, which the hook checks against the transcript. &lt;code&gt;EVIDENCE&lt;/code&gt; has to look like real failing output: a 401, a &amp;quot;not found&amp;quot;, a timeout. &lt;code&gt;PHYSICAL&lt;/code&gt; has to name hardware the agent can&apos;t touch, like a phone or a camera. Saying you&apos;re blocked isn&apos;t enough. You have to show it.&lt;/p&gt;
&lt;p&gt;This one fired the most: 158 times across 39 sessions, each a turn about to end with work still open, sent back to finish. It exits early when the event has &lt;code&gt;stop_hook_active&lt;/code&gt; set, so a refused stop can&apos;t turn into an endless loop.&lt;/p&gt;
&lt;h2 id=&quot;how-i-keep-hooks-honest&quot;&gt;How I keep hooks honest &lt;a class=&quot;anchor&quot; href=&quot;#how-i-keep-hooks-honest&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A hook is code that can block real work, so I test these like code. Each of the three has cases for what it must block and what it must let through, run with one command, and all of them pass: 5 of 5, 9 of 9, and 5 of 5. Here&apos;s part of the run for &lt;code&gt;block-unscoped-tests&lt;/code&gt;:&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;pre tabindex=&quot;0&quot; class=&quot;shiki karpinsky&quot; style=&quot;background-color:#030712;color:#e5e7eb&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ut1-run-test-dash-unscopes-blocked PASS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ut2-pnpm-run-test-full-blocked     PASS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ut3-vitest-run-bare-blocked        PASS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ut4-jest-bare-blocked              PASS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ut5-scoped-file-allowed            PASS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ut6-name-pattern-allowed           PASS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ut7-scoped-dir-allowed             PASS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;&lt;p&gt;The allowed cases matter as much as the blocked ones. &lt;code&gt;sw4-single-sleep-allowed&lt;/code&gt; and &lt;code&gt;ut5-scoped-file-allowed&lt;/code&gt; are there because a hook that blocks too much gets switched off, and then the rule is back to being a line in a prompt.&lt;/p&gt;
&lt;h2 id=&quot;where-i&apos;m-taking-hooks-next%3A-classifiers&quot;&gt;Where I&apos;m taking hooks next: classifiers &lt;a class=&quot;anchor&quot; href=&quot;#where-i&apos;m-taking-hooks-next%3A-classifiers&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Regex is the right tool for &lt;code&gt;block-spin-wait&lt;/code&gt;. Shell commands are structured, and &amp;quot;a loop with a sleep in it&amp;quot; is a pattern. The sweep is different. It reads prose, and prose has more ways to say a thing than I can list. Run against the sweep&apos;s current patterns:&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;pre tabindex=&quot;0&quot; class=&quot;shiki karpinsky&quot; style=&quot;background-color:#030712;color:#e5e7eb&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;Want me to run the migration?&quot;            BLOCK  want me to …?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;Next step is the migration.&quot;              BLOCK  next up/is/step …&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;Happy to wire up the migration too.&quot;      PASS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;The migration is the obvious follow-up.&quot;  PASS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;&lt;p&gt;The last two leave the same work undone, and the regexes miss them. The list grows one phrasing at a time.&lt;/p&gt;
&lt;p&gt;That&apos;s the job for a classification model: a model that doesn&apos;t write text, it picks an answer from a set you define. OpenAI announced one at DevDay on September 29, the &lt;a href=&quot;https://openai.com/index/devday-2026-recap/&quot;&gt;Decisions API&lt;/a&gt;, in limited preview at launch: you set the questions and the possible answers, and get back &amp;quot;answers they can use to classify content, route requests, or choose an agent&apos;s next action.&amp;quot; As of early October, OpenAI hasn&apos;t published scores, latency, or pricing for it. &lt;a href=&quot;https://docs.typesafe.ai/api.md&quot;&gt;TypeSafe&apos;s Jev&lt;/a&gt; already returns the chosen option with &amp;quot;the full probability distribution.&amp;quot;&lt;/p&gt;
&lt;p&gt;So you give it the final message and a fixed set of labels, and it answers with something like &lt;code&gt;promise: 0.91&lt;/code&gt;. The check stays mechanical. Only the matching gets smarter. A sketch of the sweep with one, where &lt;code&gt;classify&lt;/code&gt; stands for the model call:&lt;/p&gt;
&lt;figure class=&quot;code&quot;&gt;&lt;header&gt;&lt;span&gt;js&lt;/span&gt;&lt;button type=&quot;button&quot; hidden&gt;Copy&lt;/button&gt;&lt;/header&gt;&lt;pre tabindex=&quot;0&quot; class=&quot;shiki karpinsky&quot; style=&quot;background-color:#030712;color:#e5e7eb&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; LABELS &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;finished&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;promise&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;offer&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;disclaimer&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; THRESHOLD &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#BFE3B0&quot;&gt; 0.8&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; label&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; score &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#87CC70&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt; classify&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;finalMessage&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; LABELS&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; timeoutMs&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#BFE3B0&quot;&gt; 2000&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#87CC70&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;label &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;finished&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; score &lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt; THRESHOLD&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;  process&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;stderr&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;write&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;Blocked: final message reads as &quot;&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;&quot; (&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;score&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#D6CF9A&quot;&gt;).\n&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E5E7EB&quot;&gt;  process&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#F9FAFB;font-weight:bold&quot;&gt;exit&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#BFE3B0&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9CA3AF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;&lt;p&gt;The tradeoffs are real, and they decide where this goes first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Latency.&lt;/strong&gt; A &lt;code&gt;PreToolUse&lt;/code&gt; hook runs on every tool call, so a network call there is felt everywhere. A &lt;code&gt;Stop&lt;/code&gt; hook runs once per turn. That&apos;s why the sweep goes first and &lt;code&gt;block-spin-wait&lt;/code&gt; stays regex.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Every check is a paid call. Keeping the regexes as a first pass means the model only sees what they can&apos;t decide.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The threshold.&lt;/strong&gt; 0.8 is a starting point, not an answer. I&apos;ll tune it against the test cases I already have: every must-block and must-allow case is a labeled example.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When it&apos;s unsure.&lt;/strong&gt; A timeout or a low score needs a default. The sweep should fail open: the worst case is one turn that ends early, which is where I was before the hook existed. A guard on anything public should fail closed, because there the worst case can&apos;t be undone.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;which-rules-become-hooks&quot;&gt;Which rules become hooks &lt;a class=&quot;anchor&quot; href=&quot;#which-rules-become-hooks&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My test: if I&apos;d be annoyed every time the agent broke a rule, and I can recognize the break from the command or the final message, it&apos;s a hook. If it takes taste or context (naming, scope, which approach to take), it stays in the prompt. And once it&apos;s a hook, it has to keep earning its place.&lt;/p&gt;
&lt;p&gt;The three rules in this post used to be notes I kept for my agents. Now they&apos;re scripts with tests, and in September they held 327 times without me saying anything.&lt;/p&gt;
</content:encoded></item></channel></rss>