The symptom is familiar if you have hit it. I have two coding agents running, each in its own Git worktree, each pointed at a different branch. One triggers a type-check. A few minutes later the second does the same. The laptop starts sounding like it is warming up for takeoff, terminal output stalls, and the whole desktop gets sluggish. The agents are not stuck. They are both running. So is the swap partition.
The setup felt well-organized. Separate worktrees, separate branches, parallel work. What it did not account for is that worktrees do not know anything about memory.
What worktrees actually isolate
A Git worktree gives each agent its own working directory and its own checked-out branch. One .git object store, multiple working trees. From a version control perspective this is clean: the agents cannot corrupt each other's file state or accidentally work on the same branch.
The operating system sees something different. It sees processes. Each agent spawns whatever it needs: a TypeScript compiler, a test runner, a linter. Those processes compete for the same physical RAM. The kernel does not know or care that they came from different worktrees.
TypeScript checkers are not light. tsc on a mid-sized monorepo can hold several gigabytes of heap while it loads the full type graph. Vitest with a large test suite can do the same. ESLint with TypeScript-aware rules loads the full type checker internally. Node.js does not aggressively cap its own heap. V8's default heap limit on a machine with plenty of RAM sits around four gigabytes. When two agents each trigger one of these processes at once, the memory math can stop working before either finishes.
The OS handles the overflow by paging to disk. Swap is slower than RAM by several orders of magnitude, and it degrades the whole machine at once. (An unplanned test of your swap throughput is one way to spend an afternoon.) Every interactive application on the desktop stutters. The agents slow down because the OS is paging their data in and out. Neither finishes faster. They finish slower, and they take everything else with them.
The isolation worktrees provide is real and useful. It just does not extend to this part.
Remote machines
The most complete answer to this problem is to run agents somewhere else. A remote VM, a cloud dev environment, a dedicated build server: the agent runs there, its processes consume that machine's RAM, and the local machine stays responsive.
This works well for some people. If you have a cloud account with powerful VMs and a workflow that tolerates network-attached development, remote agents are genuinely good infrastructure. The swap problem becomes some other machine's problem.
But remote compute carries real costs. There is the monthly cost of the machines themselves. There is the setup time: provisioning, SSH keys, environment parity, secret management, and all the configuration that makes a remote machine feel like the local one. Many teams keep code off external infrastructure by policy. Solo developers and small teams may not want to manage a remote agent fleet on top of everything else.
There is also data locality. Some projects have licensing constraints, client data, or proprietary tooling that cannot leave the local network. "Run it in the cloud" requires more sign-off than is always available, or carries legal risk that is not worth taking for a quality-of-life improvement.
Remote machines also drift. They need updates, monitoring, and when something breaks, it is harder to debug than a process running locally. For developers who already work primarily offline or who value low-latency tool feedback, the operational overhead of remote agents can exceed the original problem in friction.
None of this is an argument against remote compute. It is a recognition that local development has real advantages and that not every team has the budget, the infrastructure access, or the appetite for the trade. The local problem is worth solving on its own terms.
Serializing the expensive parts
If the problem is concurrent heavy processes sharing memory, one solution is to not run them concurrently. Not all processes. Most agent work is lightweight in parallel: editing files, reading context, writing tests, making API calls. The expensive operations are the validation steps: the full type-check, the test suite, the lint run.
Serializing just those operations is a tractable trade. If one agent is running tsc and another wants to start its own tsc, the second one waits. Each type-check still takes the same amount of time, but they do not overlap. The machine stays responsive. Agents keep working on everything else while they wait.
The mechanism is a machine-wide lock: a single token that any process can try to acquire before running an expensive command. The lock lives outside any individual worktree, in a shared location like /tmp or a stable path in $HOME, so any agent on the machine can reach it. The process that holds the lock runs its command. Everyone else waits.
This is not a new idea. CI pipelines have used file-based mutual exclusion for a long time. What is slightly different here is applying it across independent AI coding agents that were not designed to coordinate with each other.
What the lock does not do
A lock prevents processes from overlapping. It does nothing else.
A single process holding the lock can still consume all available RAM if its heap grows unchecked. Adding --max-old-space-size to the Node.js invocation caps V8's old-space heap, but it does not cap total process RSS or native allocations, and it does not prevent the OS from swapping. When V8 exceeds the heap limit, it raises an out-of-memory error; whether that terminates the process cleanly depends on whether anything catches it. It can still be a useful signal: it removes one obvious source of unbounded heap growth and tends to make overreaching processes fail sooner. Use it as a complement to the lock, not a guarantee.
The lock also does not coordinate agents automatically. Each agent has to invoke the wrapper script rather than calling tsc or vitest directly. Agents that bypass npm scripts and invoke binaries through other paths will not acquire the lock. The approach requires each worktree's scripts to be updated to use the wrapper.
And it provides no memory isolation beyond serialization. It is not cgroups, not a container, not a memory budget. It is a queue for one category of operation. That is a much smaller guarantee than full resource isolation, but for a local multi-agent setup it is usually the right-sized one.
agent-flock
I built agent-flock to handle this. The first version is out and I am testing it in my personal repos.
It is a small native CLI that uses an OS-managed file lock. Give a command a lock name, and any other command using the same name will wait its turn. The OS releases the lock when the process exits, so there is no daemon to run and no stale-lock timer to worry about. Drop it into any worktree's npm scripts and it coordinates without any agent framework integration.
The real trade
Serializing type-checks does mean total wall-clock time goes up when agents would otherwise overlap. Two concurrent type-checks that each take a few minutes will now run back to back instead of simultaneously.
The machine stays responsive the whole time. The agents keep working on unblocked tasks. And the type-checks actually finish, rather than thrashing swap while the fans run and the whole desktop crawls.
That is the bet: that real swap degradation is worse than sequential execution. For most local multi-agent setups, it is.