DEV Community

Cover image for Does Your Git Log Look Like a Rebase Fight Club for Claude Agents?
jesse heaslip
jesse heaslip

Posted on

Does Your Git Log Look Like a Rebase Fight Club for Claude Agents?

Does any of this sound familiar?

You've got three or four Claude Code agents going at once on the same repo. Good — that's the whole point of running them in parallel. Then:

  • Your MacBook's fans spin up like a jet engine because two agents just kicked off a full build in the same ten seconds.
  • One agent's push gets rejected, it rebases and retries — and lands the retry right on top of a second agent finishing in that same window. Now you're untangling which change actually made it in.
  • A test fails, you re-run it, it passes. Not flaky. Two agents reset the same test database at the same moment and each other's runs looked broken.
  • You open the git log and it reads like an argument — three "Merge branch" commits in a row, none of them meaningful.
  • You told every agent "always land through the merge queue" in your CLAUDE.md, and one of them pushed straight to main anyway, the one time it was in a hurry.

None of that is a discipline problem. It's what happens when several fast, confident processes share one mutable thing — a branch, a CPU, a database — with nothing structurally stopping them from stepping on each other. If any of this is your afternoon, keep reading.

Or just check out Claude Code Merge Queue. It is a free, local merge queue for parallel Claude Code agents. Zero runtime deps, no PRs, no Actions bill. This piece stays in the code — the FIFO lock, the git-hook enforcement, the config shape.

Worktrees are solved. This is what isn't.

Claude Code isolates agents natively now — one flag, no setup:

claude --worktree <name>
Enter fullscreen mode Exit fullscreen mode

Since v2.1.49 (Feb 2026), that gives every session its own git worktree so concurrent agents stop clobbering each other's uncommitted edits. Good — that part's done, and if you're here for "how do I isolate my agents," you already have your answer.

What isolation doesn't solve is what happens when four isolated agents all try to land, build, and test at the same time.

  • Everyone pushes to the same branch. One wins, one gets a non-fast-forward rejection. The loser rebases and pushes again — and if a third agent lands in that same window, its retry races too. More agents means this compounds instead of resolving.
  • A full build is heavy. Run four at once and you're thrashing the same CPU/memory/disk four ways.
  • If your tests hit a shared resource — a database, a queue — concurrent runs race each other's resets. The failures look flaky. They're not. They're honest.

None of this is a skill issue on the agents' part. It's what happens when several fast, confident processes share one mutable thing with no traffic control. Telling them to "please coordinate" doesn't fix it — an agent (or a teammate in a hurry) will violate a documented convention exactly once, at exactly the wrong moment, and mean nothing by it. So don't ask nicely. Make the collision structurally impossible.

The primitive: a FIFO lock with no timeouts

Everything in this tool — the build queue, the landing queue — is the same lock wearing two hats. One queue name is one mutex for the whole repo, and every worktree of that repo reaches for the same one, so parallel agent lanes all line up behind it instead of each getting their own private illusion of exclusivity. Two unrelated repos on the same machine never see each other's locks at all.

The one design decision worth calling out: there are no timeouts, anywhere. A lock isn't released because some number of seconds went by — that's a magic constant waiting to be wrong on a slow machine or a big build. Instead, a lock is held until its owner releases it, or until another waiter notices the owner's process no longer exists. Checking whether a process is still alive is cheap and exact, so there's no staleness window to tune and no "assume it's dead after 30 seconds and hope nothing's still running."

That means kill -9-ing the process holding the lock mid-build doesn't wedge anything — the very next waiter that checks notices the process is gone and takes over immediately. No stale-lock cleanup script, no "just restart your laptop and try again."

The other piece is fairness. Waiters don't scramble for the lock the instant it frees up — whoever asked first gets it first, a strict first-in-first-out order. Otherwise whichever agent happens to poll fastest could cut the line forever, starving out an agent that's been waiting the whole time.

build-lock and land are this exact same lock under two different names — a build never contends with a landing, and vice versa.

Enforcement: a convention alone doesn't survive contact with an agent

claude-code-merge-queue land rebases your lane onto the integration branch and pushes, one lane at a time, through that lock. But "always land through here" is just a convention, and a fast agent under time pressure will skip a convention and hand-roll a plain git push exactly once, at exactly the wrong moment, meaning nothing by it.

So the actual guarantee doesn't live in an instructions file at all — it lives in a git pre-push hook, which runs on every push whether or not the agent remembers the rule. The hook blocks any direct push to the integration branch, or anything else marked protected, unless it's coming from the queue's own landing step — the only path allowed to flip the one flag that unblocks it. Every other direct push gets rejected on the spot, with the actual command to run instead of a vague warning.

That same hook is also where your real checks run — lint, typecheck, tests, build, whatever you'd trust in CI — and a push fails if they fail. Leave the check command unconfigured and it fails every push by default; the tool refuses to quietly land unverified code rather than assume you meant "skip the checks."

There's exactly one way around any of this: a single emergency environment variable for a genuine break-glass push, not a pile of overrides. Worth being honest about what that is and isn't — it stops mistakes and forgetful shortcuts, not an agent that deliberately sets that variable itself or deletes the hook. Nothing here is a security boundary. It's a coordination fix for fast, confident, forgetful agents, not a defense against a hostile one.

Why not just use GitHub's Merge Queue

GitHub ships one. Worth understanding why it's built for a different problem than "one person, several local agents":

GitHub Merge Queue Claude Code Merge Queue
Private repo Enterprise Cloud only Any plan, any repo
Cost per landing Actions minutes, every queue attempt $0 — runs on your machine
Requires A pull request Nothing — direct rebase + push

If you're solo, building fast, and the only reviewer for most of these diffs is your own test suite, a PR is ceremony with no reviewer behind it, and GitHub's 2026 pricing (a new per-minute charge on self-hosted runners as of March 2026) is trending toward more metering, not less. Skipping the PR isn't skipping review — it's swapping a human with no time to read every diff for a machine that checks every single one, the same way, every time.

Closest prior art, and where it stops

Worth naming directly rather than implying this space is empty — it isn't, the combination is what's missing.

  • tfriedel/claude-worktree-hooks covers worktree lifecycle — env files, dependency install, port assignment, cleanup — but its own README draws the line plainly: it doesn't touch build queues, merge queues, git hook enforcement, or test isolation.
  • This dev.to post takes GitHub's own branch-protection route instead — a required PR plus status checks — entirely platform-enforced, with no local queue and no mention of the Actions-minutes cost.
  • yongjip/mergetrain, released after this tool, solves a similar problem with a different shape — a SQLite-backed queue and a single lease-fenced runner that merges several agents' branches into one validated "train" before an atomic push, rather than landing one lane at a time.

What this doesn't do

  • One machine, not a fleet. The FIFO queue lives in local temp storage, keyed to the repo. It has zero reach across machines. Two laptops landing at the same instant just hit git's ordinary non-fast-forward rejection — safe, not corrupting, just uncoordinated, exactly what happens today for any team pushing to a shared branch with no queue at all.
  • Not a security boundary. Covered above — mistakes and convention drift, not an adversarial agent.
  • Guarantees a check ran, not that it's good. It enforces that checkCommand exists and passed. Whether that's a real suite or echo ok is entirely on you.
  • A real throughput ceiling. The lock holds for the entire duration of checkCommand — one landing at a time, machine-wide. A 3–4 minute suite caps you well under 20 landings/hour, flat out, before any queue-wait time on top of that.

Takeaways

  • Worktree isolation and landing coordination are two different problems — solving the first (native to Claude Code since Feb 2026) doesn't touch the second at all.
  • A FIFO lock keyed by PID-liveness instead of a timeout is the one idea underneath the whole tool: no stale-lock cleanup, no magic staleness constant to tune.
  • A convention an agent can skip isn't a guarantee — the actual enforcement lives in a git hook, not in the instructions you hand the agent.

Claude Code Merge Queue is MIT-licensed, zero runtime deps. npx claude-code-merge-queue init wires it into an existing repo in two commands. If you're already running more than one Claude Code agent against the same repo, you've probably hit some version of this — happy to hear how you're handling it in the comments.

Top comments (0)