warmtree: a pool of pre-warmed git worktrees for parallel coding agents
The first time I ran three coding agents against the same repo, each one
needed its own checkout. Fine. git worktree add exists for exactly this
and it takes about as long as a blink. Then each worktree needed
npm ci, and a .env that git doesn’t track, and I sat there watching
three progress bars install the same 457 packages into three folders.
And especially while working on larger repositories, when I asked my local
AI agents to checkout and review or locally test a 5 line fix, doing a git
worktree add can take seconds to minutes. That doesn’t just waste time, that
also takes me out of the flow state as well.
I automate everything that happens twice. This was happening several times a day.
The slow part isn’t git worktree add
Creating a worktree is a metadata operation. Git links a new working directory to the existing object store and checks out a branch. On a laptop that is milliseconds.
Everything after it is the problem. Dependencies have to be installed.
Build caches start cold. The .env and .env.local files that make the app
run locally are untracked, so they aren’t in the new tree at all. On a small
project that’s a minor annoyance. On a real one it’s minutes, and with agents
fanning out in parallel you pay it over and over.
I measured it on a private TypeScript app: 457 packages, 664 MB of
node_modules. A cold git worktree add followed by npm ci took 6.6
seconds on a fast machine with a fast network and a warm npm cache. That is
the best case. The typical case, on a corporate network with a fresh cache,
is a coffee break.
warmtree take handed over a ready workspace in 0.15 seconds.
What warmtree does
warmtree keeps a small pool of worktrees already created, dependencies installed, env files copied, and parked on your base branch with a detached HEAD. It calls them slots. I was going to stay in tree metaphor-land but I didn’t want to semantically overload terms like tree-branch. When you need a workspace, you take one:
warmtree init # writes .warmtree.toml
warmtree fill # creates and warms the slots
cd "$(warmtree take feature/login)"
# ... commit, push, open a PR ...
warmtree release feature/login
take creates the branch inside a ready slot, prints the slot’s path, and
refills the pool behind you. release parks the slot back on the base branch
and returns it to the pool with its installed dependencies intact, so the
next take is just as fast. So it’s not just about switching branches, it’s about
calling dibs or reserving/checking-out/etc a slot and also keeping it warm, or
fast-forwarded.
That’s the whole tool. warmtree owns the create and release steps and nothing else. In between, a slot is an ordinary git worktree. Your editor, your hooks, your other worktree tools, and every git command work exactly as they would anywhere else. If you already use worktrunk for hooks and port allocation and cleanup, keep using it; warmtree just replaces the create step with a warm one.
The config is one small file:
[pool]
size = 2 # slots to keep ready
lockfiles = ["package-lock.json"]
[warm]
run = ["npm ci"] # commands run inside a slot when it warms
copy = [".env", ".env.local"] # untracked files copied from the main worktree
warmtree has no idea what npm is. You tell it what to run; it runs it inside
each slot and remembers the hash of your lockfiles. A nightly
warmtree refresh fast-forwards the idle slots and re-runs the install only
where a lockfile changed.
Why this matters for agents specifically
A human developer creates a worktree a few times a week. A coding agent
fanning out on a task might need three at once, right now, and it will
happily run git worktree add and npm ci itself every single time.
So warmtree ships an Agent Skill: a short
instruction file that warmtree init drops into the right folder for
whichever agents it finds signs of in your repo, currently Claude Code, GitHub
Copilot, Codex, and Cursor. The skill teaches the agent four things. Run
warmtree which to check whether it’s already in a slot. Run warmtree size
before fanning out to see how many are ready, and grow the pool if it needs
more. Run warmtree take instead of git worktree add. Run
warmtree release when the branch is merged.
Commit that folder and every agent that touches the project gets the same behavior. This post was written in slot-1 of this site’s pool, by an agent that took the slot in 0.15 seconds and found two bugs in warmtree while doing it. Dogfooding works.
Design choices
A few decisions I’m happy with:
Slots are detached, and branches are created at take time. Git allows a branch to be checked out in only one worktree. If slots parked on a named branch, the pool would collide with your main checkout. Parking them detached on the base commit means a slot is never “on” a branch anyone is using.
Release resets instead of deleting. release checks out the base branch
detached and runs git clean -fd, which removes untracked files but keeps
ignored ones. node_modules, .venv, and their friends survive. That is what
makes the second take as fast as the first.
Warming never holds the lock. A slot is marked warming under the pool
lock, the slow install runs with the lock released, and the result is
recorded under the lock again. A three-minute npm ci in one slot never
blocks a take on another.
Two agents can take at the same moment. State is one JSON file guarded by an OS file lock. Concurrent takes get different slots. There’s a test for it, against real git repos, because nothing about git is mocked in the suite.
An empty pool is slow, not broken. If nothing is ready, take falls back
to a plain git worktree add, says so on stderr, and the new worktree joins
the pool. Agents should never fail because the pool drained.
stdout is only the path. Everything else take says goes to stderr, so
cd "$(warmtree take my-branch)" just works. Small thing. I use it forty
times a day.
Standard library only. warmtree has to run before your project’s own dependencies exist. That’s the entire point, so it has none of its own. Python 3.12 and git, nothing else.
Already have worktrees? Adopt them
Added September 11, 2026, with warmtree 0.2.0.
The first question I got after publishing this was some version of “I
already have six worktrees; do I have to throw them away?” No. The pool is
additive. init and fill don’t touch anything you made by hand, and you
can start using take for your next branch today while the old worktrees
sit where they are.
For the ones you’re still working in, there is now adopt:
cd "$(warmtree adopt ../myrepo-feature-x)"
That moves the directory into the pool as a taken slot. The branch comes
with it, so do uncommitted changes, and so do the installed dependencies.
Nothing is re-installed and nothing is reset; adoption is a move, not a
rebuild. When the branch is done, warmtree release recycles the slot
with its node_modules intact instead of you deleting the tree, which is
the whole reason to bother.
Two things to know. Because it’s a move, any editor or shell already open
on the old path needs repointing, which is why adopt prints the new path
on stdout and the cd "$(...)" idiom follows it. And it wants a branch
checked out; a detached worktree is refused until you give it one. The
main worktree can’t be adopted either, on purpose.
Branches without a checkout need no onboarding at all. They’re just refs,
and warmtree take <branch> is how they get a workspace from now on. A
worktree whose branch merged months ago isn’t worth adopting; delete it and
let a fresh slot do the work.
The agent skill knows about adopt too, with one guardrail: since it moves
directories, the skill only runs it when you ask to onboard an existing
worktree, and it tells you the new path.
Status and how to try it
It’s still early for this one. I’m dogfooding it daily on my fun side projects. As of this update it’s at 0.2.0. MIT licensed, source on GitHub, package on PyPI.
uv tool install warmtree
# or, on a machine with nothing but Python and git:
uvx warmtree status
# or straight from the repo:
uv tool install git+https://github.com/dcolliervb23/warmtree
Then, from inside a repo:
warmtree init
# put your install command in [warm] run
warmtree fill
warmtree status
If something surprises you, open an issue. The papercuts found so far came from using it, not from planning it, and I’d like more of those.
exit 0