<?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: alphanumericentity</title>
    <description>The latest articles on DEV Community by alphanumericentity (@alphanumericentity).</description>
    <link>https://dev.to/alphanumericentity</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4117272%2Ff1fcd21c-2d2f-4c99-9c20-2bfc09d4b12e.png</url>
      <title>DEV Community: alphanumericentity</title>
      <link>https://dev.to/alphanumericentity</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alphanumericentity"/>
    <language>en</language>
    <item>
      <title>The agent did exactly what I asked. That was the bug.</title>
      <dc:creator>alphanumericentity</dc:creator>
      <pubDate>Tue, 06 Oct 2026 02:21:18 +0000</pubDate>
      <link>https://dev.to/alphanumericentity/the-agent-did-exactly-what-i-asked-that-was-the-bug-3a4i</link>
      <guid>https://dev.to/alphanumericentity/the-agent-did-exactly-what-i-asked-that-was-the-bug-3a4i</guid>
      <description>&lt;h2&gt;
  
  
  Three times this month my agent executed a flawed request perfectly. Hooks can't catch this one, because the mistake isn't in any single tool call, it's in the plan. Here's what I changed instead.
&lt;/h2&gt;

&lt;p&gt;I wrote a while back about moving rules out of my instructions file and into hooks, because a rule in prose gets followed most of the time and a rule in a tripwire gets followed every time. I still believe that. Someone in the comments pushed on where determinism stops being the right tool, and I didn't have a good answer then.&lt;/p&gt;

&lt;p&gt;I do now, because three times in the past month my agent did precisely what I asked, did it well, and the result was wrong anyway. Not a hallucination, not a bad diff. Correct execution of a flawed request. That class of failure has no tool call to intercept, which makes it the exact thing a hook cannot save you from.&lt;/p&gt;

&lt;h2&gt;
  
  
  One: I asked for a demo GIF and got a security bug
&lt;/h2&gt;

&lt;p&gt;I have a side project that redacts PDFs in the browser. I wanted an animated demo for the landing page, so I asked the agent to drive the app with Playwright and record the whole flow: open a file, drag a box over a name, export, done.&lt;/p&gt;

&lt;p&gt;It built the script. The script worked. I watched the GIF and the black box was sitting next to the name instead of on top of it.&lt;/p&gt;

&lt;p&gt;The redaction code was placing boxes by splitting a text run's total width by its character count, so a box over characters 4 to 9 was a flat fraction of the run. That's correct for a monospaced font and wrong for every other font, and the drift grows with the length of the run. Sometimes it covered the word. Sometimes it covered the word and a bit of the next one. Sometimes it sat just to the right of the thing I was trying to hide.&lt;/p&gt;

&lt;p&gt;Here is the part that stings. The app already verified its own output. After exporting, it re-opens the PDF it just produced, extracts the text layer and checks that the redacted string is gone. That check passed every single time, and it was right to pass: the text really was destroyed. It just wasn't the text under the box the user drew. My verifier and my renderer were computing position the same wrong way, so the verifier could not see the error even in principle.&lt;/p&gt;

&lt;p&gt;I asked for a marketing asset. What found the bug was that a GIF is looked at by a human with eyes, and every assertion I had written shared an assumption with the code it was testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two: a perfect entry in the wrong section
&lt;/h2&gt;

&lt;p&gt;Different day. I wanted my project listed in one of the big awesome-privacy lists, so I asked the agent to add it to the Office section and open the PR.&lt;/p&gt;

&lt;p&gt;It did the work properly. Read the contributing guide, matched the entry format down to the trailing source-link convention the other entries use, filled in the seven-point checklist, picked an insertion point that wouldn't conflict with other open PRs, wrote a disclosure paragraph. Genuinely good work.&lt;/p&gt;

&lt;p&gt;I caught it at the last second, with the commit dialog open: that section is titled "Avoid Microsoft Office and Google Docs, instead use", and everything in it is a suite, an editor or a spreadsheet. A PDF utility in there is a category error. Any maintainer would have closed it, and fairly.&lt;/p&gt;

&lt;p&gt;The agent never had a reason to question the section. I named it. It optimized for "is this entry well-formed and well-argued", which it was, and never for "is Office the right section", because I'd already answered that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three: a confident click path to a setting that lives somewhere else
&lt;/h2&gt;

&lt;p&gt;Smallest one, same shape. I asked where a particular GitHub repo setting lives. I got "Settings, then General", stated plainly, with the surrounding steps.&lt;/p&gt;

&lt;p&gt;It isn't there. That field is behind a gear icon next to the About panel on the repo's front page, which is not Settings at all. The answer was assembled from what a settings page plausibly contains. When I made it go read the documentation page first, it came back with the right path in one shot, and with the character limits and naming rules I hadn't thought to ask about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these have in common
&lt;/h2&gt;

&lt;p&gt;The agent is a premise-taker. It will interrogate its own implementation all day and will not touch the frame of the request, because the frame came from you and you're the one it's trying to satisfy. "Did I do what was asked" is a question it can answer. "Was the ask right" isn't even in scope.&lt;/p&gt;

&lt;p&gt;Four things I changed, in rough order of how much they've paid off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask for the decision, not the destination.&lt;/strong&gt; "Find where this belongs in the list, then add it" instead of "add it to the Office section". One extra clause moves the category judgement inside the task, where the agent will actually spend reasoning on it. When I name the destination, I've silently converted a decision into a constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primary source, or say it's unverified.&lt;/strong&gt; For anything about a UI, a config field, an API shape, a provider's semantics: read it this session and cite it, or label the answer as a guess. Plausible-sounding click paths are the most confident wrong answers I get, because nothing in the model's experience punishes a well-formed guess about a settings page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make it tell me what it assumed.&lt;/strong&gt; Every non-trivial task ends with what it had to assume and what it could not check. This is cheap and it has caught more than I expected, because the premise shows up in writing where I can see it, instead of staying implicit in a diff that looks fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the artifact that can disagree with me.&lt;/strong&gt; A screenshot, a recording, a pixel diff, a second measurement taken a different way. The GIF found the redaction bug for one reason: it didn't share any code with the thing it was checking. Now that project has a test that pins box positions against independently computed glyph metrics and then samples the burned pixels, which is the same idea with the human taken out of the loop. It's &lt;a href="https://github.com/alphanumericentity/pdfairlock/blob/main/e2e/redact-geometry.spec.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you want the shape of it, AGPL like the rest of it. If your check is built from the same assumption as the code it checks, you don't have a check, you have a second copy of your belief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a hook can't help here
&lt;/h2&gt;

&lt;p&gt;I tried to find the tripwire for this and there isn't one, which took me a while to accept.&lt;/p&gt;

&lt;p&gt;A hook fires on a single tool call and decides from that call's input alone. That works when the mistake is local: a dangerous shell command, a comment block, an edit to a file that should never change. A wrong premise doesn't look like that. It produces a sequence of individually reasonable calls, each one defensible on its own, all of them marching toward a thing that shouldn't have been built. There's no string to match, no file path to guard, no moment where the input looks wrong.&lt;/p&gt;

&lt;p&gt;Determinism needs a decidable question. "Is the Office section the right home for a PDF tool" is not one.&lt;/p&gt;

&lt;p&gt;So the fix isn't mechanical. It's that the review has to happen earlier than I was doing it, before the work rather than on the diff. For agent work I'd now go further: reviewing the plan is worth more than reviewing the code, because the code is usually right. The expensive errors arrive fully formed, well-tested and pointed in the wrong direction.&lt;/p&gt;

&lt;p&gt;That's the part I'd like to be argued with on. Do you actually review plans before the agent starts, and does it hold up, or does it just move the rubber stamp one step earlier? Because I notice I'm much better at approving a plan I half-read than at approving a diff I half-read, and I don't think that's a good sign.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your agent's instructions file is a suggestion. A hook is a contract.</title>
      <dc:creator>alphanumericentity</dc:creator>
      <pubDate>Fri, 02 Oct 2026 09:12:06 +0000</pubDate>
      <link>https://dev.to/alphanumericentity/your-agents-instructions-file-is-a-suggestion-a-hook-is-a-contract-1eak</link>
      <guid>https://dev.to/alphanumericentity/your-agents-instructions-file-is-a-suggestion-a-hook-is-a-contract-1eak</guid>
      <description>&lt;p&gt;Every coding agent now reads an instructions file. &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;.cursorrules&lt;/code&gt;, whatever yours is called. Mine is long. It has been long for months, because every time something went wrong I did the obvious thing and wrote a sentence about it.&lt;/p&gt;

&lt;p&gt;Here is what I eventually noticed. The agent follows those sentences most of the time. Not always. And the times it doesn't are not random: they're the moments when the file is long, the task is interesting, and the rule is the least salient thing in a 30k-token context. Which is exactly when you need the rule.&lt;/p&gt;

&lt;p&gt;A rule in prose competes with every other sentence in the file for attention. A rule in a hook does not compete with anything. It just runs.&lt;/p&gt;

&lt;p&gt;So over a few months I moved the rules that actually mattered out of the instructions file and into &lt;code&gt;PreToolUse&lt;/code&gt; hooks. There are about a dozen now. The agent is noticeably less annoying to supervise, and I stopped repeating myself.&lt;/p&gt;

&lt;p&gt;The examples below are Claude Code hooks, because that's what I use. The idea is the part worth stealing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanics are almost insultingly simple
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;PreToolUse&lt;/code&gt; hook is a program. The agent is about to call a tool, your program gets the call as JSON on stdin, and you decide.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;psql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No DELETE against this database from an agent session. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write the statement into a migration and let a human run it.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make this better than a code review or a nag in the prompt:&lt;/p&gt;

&lt;p&gt;The call never happens. Not "the model is discouraged from it". The tool returns your refusal instead of running.&lt;/p&gt;

&lt;p&gt;And the &lt;code&gt;reason&lt;/code&gt; goes back to the model, not to you. That's the part I underrated. A good reason makes the agent correct itself and carry on, so a blocked call costs a few seconds instead of a round trip through your attention. A bad reason ("blocked") makes it guess, flail, or ask you. The reason string is the whole product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tripwires
&lt;/h2&gt;

&lt;p&gt;Grouped by what they're actually protecting, because the categories turned out to matter more than the specifics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things that can't be undone.&lt;/strong&gt; A guard that makes writes to the production database impossible from any agent session: mass deletes, truncates, cache flushes, a session-level &lt;code&gt;SET&lt;/code&gt; through a connection pooler. A guard that blocks merge commands, because merging is a human's decision and an agent that can merge will eventually merge something at 2am. A guard that refuses &lt;code&gt;sed -i&lt;/code&gt; into source files, which is how an agent rewrites a file without the edit ever passing through the checks on the edit tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things that are expensive.&lt;/strong&gt; A guard that blocks local type-checking entirely. That one sounds mad until you know that a cold run across this monorepo costs about fifty seconds of CPU and one process peaked at 5.6 GB, and that several agents in several worktrees will happily do it at once on a 24 GB laptop. CI type-checks every push anyway. The hook's reason says so, so the agent commits instead of grinding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things that are silently wrong.&lt;/strong&gt; My favourite hook, and the shortest. Our analytics query language reads a bare timestamp literal in the project's timezone, not UTC. Write &lt;code&gt;timestamp &amp;gt; '2026-07-03 00:00'&lt;/code&gt; and you have quietly shifted your window by ten hours. It once made a feature flag look like it was ramping when it wasn't, which produced a confident wrong diagnosis and a needless experiment reset. Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(timestamp|created_at|_at)\s*[&amp;lt;&amp;gt;=]+\s*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(\d{4}-\d{2}-\d{2}[^&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]*)&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([+-]\d{2}:?\d{2}|Z)\s*$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# explicit offset, fine
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TZ_TRAP_EXPLANATION&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forty lines, including the explanation. That class of bug cannot reach me any more, from any session, in any file or shell command, whether or not anybody remembered the rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things that are about taste, and therefore hopeless in prose.&lt;/strong&gt; A guard that blocks any edit adding a multi-line comment block. I had written "do not write comments" in the instructions file in capital letters, with reasons, twice. It did not work reliably, because the model's prior for "explain this clever bit" is strong and my sentence was one sentence. The hook works every time, and its reason tells the agent to rewrite the block as one line rather than just deleting the thought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things about process.&lt;/strong&gt; A guard on new UI that requires a stable test id, because the e2e suite selects by test id and a renamed element silently stops testing anything. And a guard that refuses to push a branch until an adversarial review receipt exists for the current head. The second one is the most aggressive thing in my setup, and I'd defend it: an agent that is sure its work is ready is not evidence that the work is ready.&lt;/p&gt;

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

&lt;p&gt;I don't want to sell this as free.&lt;/p&gt;

&lt;p&gt;False positives are real and they're the price. My shell guards block patterns that are usually footguns and occasionally the right thing, and then I'm reading a refusal about a command I meant to run. The honest fix is to go edit the hook, which I do, and which is still cheaper than the alternative because the fix is a diff somebody can review rather than another sentence in a file nobody re-reads.&lt;/p&gt;

&lt;p&gt;They're also per-repo and they rot. A hook that encodes "CI runs this check" is wrong the day CI stops. Mine have tests, which felt absurd when I wrote them and does not any more.&lt;/p&gt;

&lt;p&gt;And they don't replace the instructions file. Taste, context, architecture, the reason a weird thing is weird: that's all still prose, and prose is still the right shape for it. Hooks are for the small number of rules where "usually" isn't good enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I actually changed my mind about
&lt;/h2&gt;

&lt;p&gt;I started doing this because I didn't trust the model. That was the wrong frame, and it made me write mean little guards with bad reasons.&lt;/p&gt;

&lt;p&gt;The better frame is that an agent has no way to know which of your forty rules is the one that costs money. You do. A hook is how you say "this one is not a preference", and a good reason string is how you teach instead of scold. The agent reads it, understands why, fixes its own command, and moves on. That's a better loop than any amount of bold text in a markdown file.&lt;/p&gt;

&lt;p&gt;So: what's on your deny list? I'm especially curious about the opposite view, because one rule I keep going back and forth on is whether blocking &lt;code&gt;git push&lt;/code&gt; crosses from guardrail into busywork. Tell me I'm wrong about that one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A SELECT that returned more rows than its LIMIT</title>
      <dc:creator>alphanumericentity</dc:creator>
      <pubDate>Wed, 30 Sep 2026 10:11:54 +0000</pubDate>
      <link>https://dev.to/alphanumericentity/a-select-that-returned-more-rows-than-its-limit-42kl</link>
      <guid>https://dev.to/alphanumericentity/a-select-that-returned-more-rows-than-its-limit-42kl</guid>
      <description>&lt;p&gt;A scheduled job in one of our services started crashing every few days with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Cannot read properties of undefined (reading 'id')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a &lt;code&gt;for..of&lt;/code&gt; over the rows of a query. Not on the first row, not on every run, and never reproducible locally.&lt;/p&gt;

&lt;p&gt;The other clue turned up in a log line from the same job, which prints how many pending records it picked up. The query has &lt;code&gt;LIMIT 500&lt;/code&gt;. The log said &lt;code&gt;pendingTotal: 534&lt;/code&gt;. Another run said 813.&lt;/p&gt;

&lt;p&gt;A SELECT with a LIMIT of 500 returning 813 rows is the kind of thing that makes you go back and read your own SQL four times. The SQL was fine. So was the ORM. The bug was one line below the driver's surface, and it has been open upstream for about a year.&lt;/p&gt;

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

&lt;p&gt;We use &lt;a href="https://github.com/porsager/postgres" rel="noopener noreferrer"&gt;postgres-js&lt;/a&gt; (the &lt;code&gt;postgres&lt;/code&gt; package) with a connection pool, like most people do. Here's the shape of the bug, straight out of &lt;code&gt;src/connection.js&lt;/code&gt; in the current release:&lt;/p&gt;

&lt;p&gt;A connection keeps a single write cursor for the result it's filling in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;DataRow&lt;/code&gt; message the server sends gets written at that cursor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DataRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rows&lt;/code&gt; goes back to zero in exactly two places: &lt;code&gt;CommandComplete&lt;/code&gt;, which is the server saying the query finished, and &lt;code&gt;PortalSuspended&lt;/code&gt;, which is the cursor case. That's it.&lt;/p&gt;

&lt;p&gt;Now look at what happens when a query &lt;em&gt;doesn't&lt;/em&gt; finish. &lt;code&gt;ErrorResponse&lt;/code&gt; records the error and returns. Then &lt;code&gt;ReadyForQuery&lt;/code&gt; cleans up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ReadyForQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... resolve or reject the query ...&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;errorResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;connectTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It allocates a fresh result array. One line later than where the cursor should have been reset, and the cursor isn't in that list.&lt;/p&gt;

&lt;p&gt;So: a query streams 97 rows, then the server kills it. &lt;code&gt;rows&lt;/code&gt; is left at 97. The connection goes back in the pool. The next query that lands on it gets a brand new empty array, writes its two rows at index 97 and 98, and resolves with an array whose &lt;code&gt;length&lt;/code&gt; is 99 and whose first 97 slots are holes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this one is nastier than a normal wrong answer
&lt;/h2&gt;

&lt;p&gt;A sparse array is a bad failure mode because nothing about it throws:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JSON.stringify(rows)&lt;/code&gt; gives you a list starting with 97 &lt;code&gt;null&lt;/code&gt;s. That goes straight into a log line or an API response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;for..of&lt;/code&gt; and &lt;code&gt;.entries()&lt;/code&gt; hand you &lt;code&gt;undefined&lt;/code&gt;, which is where our TypeError came from.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.length&lt;/code&gt; is 99, which is how a &lt;code&gt;LIMIT 500&lt;/code&gt; query reports 534.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.map()&lt;/code&gt; preserves the holes, so an ORM mapping layer passes them through.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.filter(Boolean)&lt;/code&gt; hides it completely, so whether you ever find out depends on which array method you happened to use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the quiet one, which is the reason I'm writing this down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;row&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, the record exists, and you take the "not found" branch. No error, no log, just a wrong decision. We had a version of that on a write path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trigger is almost always a statement timeout
&lt;/h2&gt;

&lt;p&gt;To hit this you need a query that errors &lt;em&gt;after&lt;/em&gt; it has streamed at least one row. The common way is &lt;code&gt;statement_timeout&lt;/code&gt;, Postgres error 57014, firing mid-scan on a query that's already returning rows.&lt;/p&gt;

&lt;p&gt;The thing to check is where your timeout is set. Ours is on the database role, not in application code, which means any slow query in any service arms this for the next query on that connection. One heavy scan in a cron job, and an unrelated request handler sharing the pool gets the holes. That's also why it never reproduced locally: on a laptop nothing takes long enough to get cancelled.&lt;/p&gt;

&lt;p&gt;If you want to confirm it in your own logs, look for these two together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A count in a log line that exceeds the &lt;code&gt;LIMIT&lt;/code&gt; of the query it came from.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;TypeError&lt;/code&gt; on &lt;code&gt;undefined&lt;/code&gt; inside a loop over query results, within a few seconds of a &lt;code&gt;canceling statement due to statement timeout&lt;/code&gt; in the same process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same process matters, since the pool lives in the process. Different pod, different pool, unrelated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading doesn't fix it
&lt;/h2&gt;

&lt;p&gt;I checked the current release and master while writing this. &lt;code&gt;postgres@3.4.9&lt;/code&gt; is the latest on npm, and both it and master reset the cursor in the same two places and no others. The fix is a one-line PR, &lt;a href="https://github.com/porsager/postgres/pull/1120" rel="noopener noreferrer"&gt;#1120&lt;/a&gt;, open since October 2025. The mechanism is written up in &lt;a href="https://github.com/porsager/postgres/issues/1181" rel="noopener noreferrer"&gt;issue #1181&lt;/a&gt;, which is open and still getting comments this week.&lt;/p&gt;

&lt;p&gt;So until it lands, patch it. With pnpm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm patch postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the one line to &lt;code&gt;ReadyForQuery&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;   query = results = errorResponse = null
   result = new Result()
&lt;span class="gi"&gt;+  rows = 0
&lt;/span&gt;   connectTimer.cancel()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;pnpm patch-commit &amp;lt;dir&amp;gt;&lt;/code&gt;, which writes the patch file and the &lt;code&gt;patchedDependencies&lt;/code&gt; entry into your workspace. npm and yarn both have their own equivalents.&lt;/p&gt;

&lt;p&gt;If you'd rather not patch a dependency, the other mitigations are all worse but worth knowing: set &lt;code&gt;max: 1&lt;/code&gt; so a poisoned connection only hurts the code that poisoned it (it doesn't make the problem go away), drop your &lt;code&gt;statement_timeout&lt;/code&gt; low enough that queries die before streaming anything (good luck), or check &lt;code&gt;rows.length&lt;/code&gt; against your own LIMIT on every read and throw. We patched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson I took
&lt;/h2&gt;

&lt;p&gt;I had a rule in my head that an error on a connection is contained: the transaction rolls back, the pool hands the connection to someone else, life continues. This was a reminder that the &lt;em&gt;client library&lt;/em&gt; also has per-connection state, and an error path is exactly where that state stops being maintained.&lt;/p&gt;

&lt;p&gt;So when a client hands you a fresh object for each query, the question worth asking is whether anything else is still pointing into the old one.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>node</category>
      <category>javascript</category>
      <category>database</category>
    </item>
    <item>
      <title>Give every coding agent its own git worktree</title>
      <dc:creator>alphanumericentity</dc:creator>
      <pubDate>Wed, 16 Sep 2026 02:58:28 +0000</pubDate>
      <link>https://dev.to/alphanumericentity/give-every-coding-agent-its-own-git-worktree-578p</link>
      <guid>https://dev.to/alphanumericentity/give-every-coding-agent-its-own-git-worktree-578p</guid>
      <description>&lt;p&gt;Quick scenario. You've got two coding agents going in two terminals. Agent one is halfway through a refactor. Agent two gets asked to fix a bug that lives on another branch, so it does the obvious thing and runs &lt;code&gt;git checkout&lt;/code&gt;. Agent one's next tool call reads files that aren't there anymore. Neither of them notices. Both keep going.&lt;/p&gt;

&lt;p&gt;I've had some version of this happen more than once before I stopped and fixed it properly. The problem is that a git checkout was designed for one person with one intention at a time. You'd never switch branches while a coworker was typing in the same directory, but an agent has no concept of "someone else is using this". It assumes the tree it saw a minute ago is the tree it has now. Often that's just false.&lt;/p&gt;

&lt;p&gt;The fix turned out to be a git feature I'd basically ignored for ten years: worktrees. They've been in git since 2.5 (2015). One repo, multiple working directories, each on a seperate branch, all sharing the same object store. One human rarely needs two checkouts of the same repo so nobody bothered. A handful of agents need exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ok so what's a worktree
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin main
git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; fix/login-redirect ../myrepo-wt-login-redirect origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes a branch off the current &lt;code&gt;origin/main&lt;/code&gt;, checks it out in a sibling directory, and registers it with the repo. Leave off the &lt;code&gt;-b&lt;/code&gt; if the branch already exists. &lt;code&gt;git worktree list&lt;/code&gt; shows you all of them, &lt;code&gt;git worktree remove ../myrepo-wt-login-redirect&lt;/code&gt; gets rid of one.&lt;/p&gt;

&lt;p&gt;Same &lt;code&gt;.git&lt;/code&gt;, many directories. A branch can only be checked out in one worktree at a time, which sounds like a limitation but is actually the whole point. The second agent that tries to grab a branch gets an error instead of silently pulling the rug out.&lt;/p&gt;

&lt;p&gt;One thing: put the worktree next to the repo, not inside it. If it's inside, the main checkout sees a big new untracked directory and sooner or later someone's &lt;code&gt;git add .&lt;/code&gt; swallows it, and that's a fun one to untangle.&lt;/p&gt;

&lt;p&gt;That's the mechanism. The mechanism alone doesn't save you though, the rules around it do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four rules
&lt;/h2&gt;

&lt;p&gt;These live in the agent's instruction file (CLAUDE.md, AGENTS.md, whatever your tool reads). Not in my head, because I forget, and the agent definitely doesn't read my head.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Isolate.&lt;/strong&gt; Every edit happens in a dedicated worktree created off the exact base you mean. New change? Fetch and base it off &lt;code&gt;origin/main&lt;/code&gt;. Fixing an open PR? Base it off that PR's current remote head, not whatever stale local copy of the branch happens to be lying around. The main checkout is read only for agents. Grep in it, &lt;code&gt;git log&lt;/code&gt; in it, don't edit in it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stage by path.&lt;/strong&gt; &lt;code&gt;git add -A&lt;/code&gt; and &lt;code&gt;git add .&lt;/code&gt; are banned. The tree might have another agent's half done work in it, or a stray &lt;code&gt;.env&lt;/code&gt;, or some debug script. An agent that stages everything commits everything. It names the files it touched and those go in, nothing else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check the diff. Twice.&lt;/strong&gt; Before pushing, &lt;code&gt;git diff origin/main...HEAD&lt;/code&gt; (three dots, not two, this matters) has to be exactly the change you intended, nothing extra pulled in and nothing missing. Then again before merging, because main moved while the agent was working and the diff that got reviewed at push time is not the diff that actually lands. If anything unexpected shows up the agent stops and says so. It does not merge and hope.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clean up.&lt;/strong&gt; &lt;code&gt;git worktree remove&lt;/code&gt; once the PR is merged, then delete the branch. Old worktrees that outlive their PR are exactly where the next agent finds a stale branch and builds on top of it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's roughly what's in my instructions file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Changing code&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never edit in the main checkout. Work in a worktree off the intended base:
  git -C /abs/path/to/repo fetch origin main
  git -C /abs/path/to/repo worktree add -b &lt;span class="nt"&gt;&amp;lt;branch&amp;gt;&lt;/span&gt; /abs/path/to/repo-wt-&lt;span class="nt"&gt;&amp;lt;slug&amp;gt;&lt;/span&gt; origin/main
&lt;span class="p"&gt;-&lt;/span&gt; Stage by path. &lt;span class="sb"&gt;`git add -A`&lt;/span&gt; and &lt;span class="sb"&gt;`git add .`&lt;/span&gt; are forbidden.
&lt;span class="p"&gt;-&lt;/span&gt; Before pushing, and again before merging, &lt;span class="sb"&gt;`git diff origin/main...HEAD`&lt;/span&gt;
  must be exactly the intended change. Anything else: stop and report.
&lt;span class="p"&gt;-&lt;/span&gt; Update a pushed branch by merging main into it, never by rebasing.
&lt;span class="p"&gt;-&lt;/span&gt; Remove the worktree once the PR is merged.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in there are on purpose.&lt;/p&gt;

&lt;p&gt;Absolute paths and &lt;code&gt;git -C&lt;/code&gt; everywhere. An agent's working directory is not something you want to depend on. Some harnesses reset it between tool calls, some (Claude Code, in my setup) prompt you every time a command starts with &lt;code&gt;cd&lt;/code&gt;, and an agent that thinks it's in the worktree but is actually sitting in the main checkout will do precisely the thing the worktree was supposed to prevent. Absolute paths make that whole class of bug impossible.&lt;/p&gt;

&lt;p&gt;And merge, don't rebase, once a branch has been pushed. A rebase rewrites commits that already exist on the remote, then needs a force push, and the force push throws away anything a human added to the branch in the meantime. An "update branch" click, a review fix, a commit from a coworker who had no idea an agent was on that branch. I know rebasing gives you a prettier history. I don't care anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use branches?
&lt;/h2&gt;

&lt;p&gt;Fair question, git already has branches.&lt;/p&gt;

&lt;p&gt;Because switching a branch is a write to a shared mutable global. Every agent in that checkout sees it and none of them get told. It also trashes everything in the tree that's keyed to its contents: build output, the bundler cache, tsc's incremental state, &lt;code&gt;node_modules&lt;/code&gt; if the branches have different deps. Two agents ping ponging between two branches in one directory spend most of their time rebuilding the world and very little of it doing the task.&lt;/p&gt;

&lt;p&gt;Also you can't run two dev servers from two branches out of one directory. With worktrees you can, and you'll want to, because "does the fix actually work" means running the thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff that will bite you
&lt;/h2&gt;

&lt;p&gt;The mechanism is simple. The edges are where the afternoon goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Untracked files don't come along.&lt;/strong&gt; A new worktree is a clean checkout of tracked files only. Your &lt;code&gt;.env&lt;/code&gt;, local config, that cert you generated months ago, none of it is there. Copy what the tree needs over explicitly. And don't let the agent "fix" a missing &lt;code&gt;.env&lt;/code&gt; by committing one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;node_modules&lt;/code&gt; is per worktree.&lt;/strong&gt; Every tree needs its own install. With pnpm this is nearly free, packages get hardlinked out of the global content addressable store so a second install is mostly bookkeeping. With npm it's a full copy every time, which adds up fast. If your monorepo has a build cache, the local part of it is per tree as well, so a remote cache is what makes worktree number two start fast rather than rebuild everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ports collide.&lt;/strong&gt; Two worktrees, same app, same port. Derive the port from the worktree name or pass it explicitly, and make the agent print the URL it actually ended up on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The database is the next shared global.&lt;/strong&gt; Two agents, two worktrees, one local Postgres. The moment one of them runs a migration the other's app is talking to a schema it doesn't expect. Per branch databases (a database or schema named after the branch, created on first boot) close this hole. It's the same move as the worktree, one level down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hooks and config are shared.&lt;/strong&gt; Every worktree reads the same &lt;code&gt;.git/hooks&lt;/code&gt; and the same repo config. Good, the guard rails apply everywhere. But a hook that assumes it's running from the main checkout path will do weird things in a worktree. Use &lt;code&gt;git rev-parse --show-toplevel&lt;/code&gt; in hooks, never a hardcoded path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stashes are shared too.&lt;/strong&gt; The stash is a ref on the repo, not the worktree. An agent that stashes in one tree leaves a lovely surprise for whoever runs &lt;code&gt;git stash pop&lt;/code&gt; in another. Honestly just don't stash. Commit the WIP on the branch and squash it later, or don't, nobody cares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lockfiles diverge.&lt;/strong&gt; Two worktrees each add a dependency and now the lockfile conflicts at merge time. Never hand merge a lockfile. Take either side, run install, commit whatever it produces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strays pile up.&lt;/strong&gt; If you &lt;code&gt;rm -rf&lt;/code&gt; a worktree directory git still thinks it's there. &lt;code&gt;git worktree prune&lt;/code&gt; clears the bookkeeping. &lt;code&gt;git worktree list&lt;/code&gt; is the first thing I run when anything git related is being weird. A branch that's checked out in some worktree also can't be deleted or checked out anywhere else until that worktree is gone, which is the first thing to check when an agent complains that a branch is "already checked out".&lt;/p&gt;

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

&lt;p&gt;Before, the checkout was a global. Everything in my agent setup had to reason about its state and none of it could, because none of them could see the others.&lt;/p&gt;

&lt;p&gt;Now the checkout is a parameter. Each agent gets its own, does its work there, proves what it did with a diff against the base, and the directory goes away when the PR merges. The main checkout just sits on &lt;code&gt;main&lt;/code&gt;, clean, and I mostly use it to read code.&lt;/p&gt;

&lt;p&gt;None of this is clever. It's one git command that's been sitting there for a decade plus four rules written down somewhere the agent will actually read them. Most of the damage agents were doing to each other's work just stopped, and whatever's left at least shows up in the diff.&lt;/p&gt;

</description>
      <category>git</category>
      <category>programming</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
