<?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>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>
