Run three AI coding agents on one machine, each in its own git worktree, and the code stays separate. Everything around the code does not.
Two dev servers try to bind port 3000, and one of them fails. Two tasks write to the same database, so one agent's test rows appear in the other agent's run. Three builds start at the same moment and the machine freezes. Finished task folders stay on disk, while package caches, browser downloads and temp files fill the drive.
Git keeps the files apart. It does not keep ports, databases, processes or disk space apart. You can fix most of this with no tool at all. Here is the manual version first.
Step 1: one worktree per task
git worktree add -b feature-login ../myapp-feature-login main
cd ../myapp-feature-login
-b creates a new branch from main. Each task gets its own folder and branch. Your main checkout stays as it is.
Step 2: one env file per task, with its own port
Create .env.local in each worktree:
PORT=3101
DATABASE_URL=postgres://localhost/myapp_feature_login
The next task gets PORT=3102. Check how your dev server reads its port. Some frameworks read .env.local. Others take a flag.
Step 3: one database per task, inside one Postgres server
createdb myapp_feature_login
Or, in psql: CREATE DATABASE myapp_feature_login;. Run the migrations against that database. One server holds all the tasks. You do not start a second Postgres instance for each agent.
Step 4: run heavy commands one at a time
On Linux, flock takes an exclusive lock on a file. The kernel drops the lock when the process dies, so a crash leaves no stale lock:
flock /tmp/build.lock npm run build
flock waits until the lock is free. Stock macOS has no flock. On macOS and in Git Bash, use a directory lock. mkdir either creates the folder or fails, so only one shell wins:
until mkdir /tmp/build.lock 2>/dev/null; do sleep 2; done
npm run build
rmdir /tmp/build.lock
The crash caveat applies to mkdir only. If the build crashes or you close the terminal, rmdir never runs. Delete /tmp/build.lock by hand before the next build.
agentgc run uses OS file locks (flock on Unix, LockFileEx on Windows), so it has no stale lock.
What gets hard by hand
Steps 1 to 4 hold up while the task count stays small. As it grows, four chores pile up:
-
Cleanup.
git worktree removerefuses while a shell or an editor sits in the folder. On Windows, the folder stays locked until you find the program that holds it. - Port bookkeeping. You keep a list of which port belongs to which task. A forgotten dev server keeps its port after its task ends.
-
Killing the right process. On Windows,
netstat -ano | findstr :3101shows the PID. Then runtaskkill /PID <pid> /F. Check the PID first. A wrong PID stops someone else's work. -
Dependencies. Each worktree holds its own
node_modules,.venvortargetfolder. The disk use adds up for every task.
The same steps in agentgc
agentgc is a garbage collector for the disk junk that CLI coding agents and dev tools leave behind: package caches, browser downloads, agent sessions and temp files. It also builds task environments: one worktree, free ports, one database name per task and an env file. The task commands create all of that in one step. The run command queues heavy work, and up starts the services of a task.
Download an archive for Windows, Linux or macOS (Apple Silicon) from the latest release. Or build it from a clone of the repo with Rust: cargo install --path .
Put the task settings in agentgc.task.toml in the repo root. A short version:
[worktree]
dir = "worktrees"
base = "main"
[copy]
files = [".env.local"]
[ports]
names = ["web"]
range = [3100, 3999]
[db]
name = "{repo}_{name}"
create = "createdb {db}"
drop = "dropdb {db}"
[env.dynamic]
DATABASE_URL = "postgres://localhost/{db}"
[setup]
commands = ["npm install"]
dir = "worktrees" puts the worktrees in a folder inside the repo, so add that folder to .gitignore. agentgc prints a hint when the folder is not ignored. It never edits .gitignore itself.
The commands:
agentgc scan # read every path before any clean
agentgc task new feature-login # dry run: prints the plan
agentgc task new feature-login --yes --run-setup # worktree, ports, database, setup
agentgc task list # ports, database and status per task
agentgc ps # processes per task, with RAM
agentgc ps --stop # dry run: idle dev servers, tests, builds
agentgc run -- npm test # queued: heavy commands take turns
agentgc clean --dry-run # junk from scan, item by item
agentgc task done feature-login # dry run: blockers and processes
agentgc task done feature-login --yes # removes worktree, task files, merged branch
task new without --yes only prints the plan. The file .agentgc/task.env holds PORT_WEB and DATABASE_URL, so your dev server must read that file.
Safety model
Only two commands delete: task done --yes and clean.
-
Dry run first.
task new,task doneandps --stopprint a plan and change nothing until you add--yes. -
Blockers stop
task done. It refuses while the worktree holds uncommitted files or commits that are neither merged nor pushed.--forceoverrides both checks and prints what would be lost. -
Item by item.
cleanasks about each item. A safe item needs ay. A risky item needs you to type its ID. Risky items go to the Recycle Bin, not to permanent delete. -
Guards.
cleanrefuses paths that hold a.gitentry, and paths named Documents, Downloads or User Data, among others. -
Idle only.
ps --stoptouches only idle dev servers, language servers, tests and builds. Agents, databases and other processes keep running. -
Config runs commands.
create,dropandsetup.commandsrun as you, without a shell. Readagentgc.task.tomllike a Makefile before you use--run-setuportask done --yes.
Honest limits
- Built and covered by automated tests on Windows, Linux and macOS in GitHub Actions. Windows is the only system where the full flow was run end to end. The release binaries are not code signed.
-
agentgc upstarts the services listed for a task. It is opt-in and installs no system service. Nothing starts at boot or login. - agentgc never starts containers, never runs Docker and never talks to a database server.
- On Windows,
ps --stop --yesstops nothing without--force, because agentgc has no graceful stop on Windows. -
runis not fair. Whoever finds a free slot first gets it.
Proof
After one cleanup session on 2026-11-04, free space on my Windows machine went from about 6 GB to about 180 GB. This is my own statement. It is not a measurement by agentgc.
Credit
The queue idea behind run comes from heavy by mokevnin, a counting semaphore for macOS and Linux. agentgc's run is separate code. It adds Windows support and named resources, such as one phone or one browser profile.
Next step
If you run agents in parallel and want fewer collisions, star the repo: https://github.com/JohnWayneeee/agentgc
Telegram channel https://t.me/+pWGhdQdw5DZmYzYy
Top comments (6)
The port + database split is the part I wish I had taken seriously earlier. I ran two agents in separate worktrees last month and still watched them fight over the same local Postgres database and a shared Redis cache — the code was isolated, but a migration from one task dropped a column the other task still expected, and I spent an evening untangling which branch had actually run it. Since then I give each worktree its own database name and port range in a small setup script, plus a one-line check that prints the active port before starting a dev server. One question: how do you handle the case where a task also needs a shared external service, like a sandbox API key with a rate limit — do you queue those calls too, or just accept that some collisions live outside git?
agentgc run --resource NAME --slots N -- caps how many commands on that resource run at once, across all worktrees. With --slots 1, they run one at a time. It is a concurrency cap, not a rate limit, and it only covers commands started through agentgc run. agentgc does not throttle the calls your app makes. Ports and databases already stay separate per worktree, since task new picks a free port and a database name for each task.
That clears it up — --slots is a concurrency cap, not a rate limit, and it only covers commands launched through agentgc run, so the app's own calls to the shared key are still unthrottled. Same shape as the database collision I ran into: the code was isolated, the shared thing wasn't. Do you add a second layer for the key itself — a small local proxy, or per-worktree keys — or just eat the 429s and let backoff sort it out?
Prove port and database bookkeeping survive a crash, since flock and mkdir already disagree there. A lock the kernel drops does not need anyone to remember to delete a folder. Every heavy command going through it, including the ones an agent runs on its own initiative, is what keeps it there in this case. The moment one of them calls the build directly, the queue is decoration. What stops an agent going around run?
The mkdir lock for heavy builds is simple, but a crash leaves a dead lockfile that halts subsequent runs. On Linux, flock binds the mutex to the file descriptor instead (flock /tmp/build.lock npm run build), so the kernel releases it automatically if the agent or build aborts.
For dependencies across parallel worktrees, pnpm or uv avoids the disk hit by hardlinking against a single global store. That stops three parallel branches from chewing through 15 GB of duplicate dependencies.
Agreed on the lock. The manual example in the article uses mkdir, which leaves a stale lock after a crash. agentgc's run takes OS file locks instead (flock on Unix, LockFileEx on Windows), so the OS releases the lock when the holder dies. A shared pnpm or uv store is on the roadmap, but nothing is built yet. The first step is a report of how much disk it would save.