Git Worktrees Changed How I Use Coding Agents
How I use Git worktrees, small scripts, and OpenSpec to give coding agents isolated feature workspaces for parallel development.
I knew about Git worktrees for years, but, to be honest, never used them.
Switching branches was usually enough. If I needed to experiment with something, I could create another branch, do the work, and switch back.
Coding agents changed that.
More specifically, working with agents through a spec-driven workflow changed that.
I often have several features moving at the same time, but they are not necessarily in the same phase.
One feature can already be in development, where the agent mostly works on its own and doesn’t need much attention from me.
At the same time, another feature can be in specification or review, where I’m much more involved: refining requirements, checking decisions, discussing tradeoffs, or reviewing the generated plan.
That is where a single working directory becomes limiting.
I don’t necessarily want two agents actively changing the same checkout. I want several independent feature workspaces that can move through different stages at the same time.
Git worktrees turned out to be a very simple way to do that.
One feature, one workspace
The basic idea of a worktree is simple: you can have multiple working directories connected to the same Git repository.
Instead of this:
project/
where I constantly switch branches, I can have:
project/
worktrees/
|-- fix-auth/
|-- add-export/
|-- experiment-api/
|-- ...
Each directory has its own checked-out branch and filesystem state.
For coding agents, this is almost exactly what I want.
One feature may be in active implementation:
fix-auth/ ← agent is working mostly independently
while another is still in a stage where I need to be involved:
add-export/ ← specification / review / decisions
The important part is not simply running more agents.
It’s being able to move several features forward at different speeds without constantly switching the state of one shared checkout.
How I organize projects
I usually keep the main repository inside a small parent directory:
project-name-meta/
├── project-name/
├── worktrees/
└── ...
project-name is the actual Git repository.
The worktrees directory sits next to it, not inside it.
After creating a few worktrees it might look like this:
project-name-meta/
├── project-name/
└── worktrees/
├── fix-auth/
├── add-export/
└── experiment-api/
I prefer this over putting worktrees inside the project repository.
The main checkout stays clean, all temporary workspaces have one obvious location, and deleting them later is simple.
Inside the repository itself, I keep a few small scripts:
project-name/
├── apps/
├── packages/
├── tools/
│ └── scripts/
│ ├── create-worktree.sh
│ ├── delete-worktree.sh
│ └── copy-worktree-includes.sh
├── .worktreeinclude
└── ...
I keep this tooling in the repository rather than installing it globally. The workflow is part of the project and can evolve with it.
The problem with a fresh worktree
Creating a worktree itself is easy:
git worktree add ../worktrees/fix-auth -b fix-auth
But a fresh Git checkout is often not enough to actually run the project.
Most projects have local files that intentionally aren’t committed:
.env
.env.local
.claude/settings.local.json
There may also be local configuration for development tools or coding agents.
Git doesn’t copy those files when it creates a worktree.
So the first few times I used worktrees, I ended up manually copying configuration from the main repository.
That gets annoying very quickly.
.worktreeinclude
I solved this with a small convention called .worktreeinclude.
This isn’t a Git feature. It’s just a file I keep in the repository that describes which local files should be copied into every new worktree.
For example:
.env
.env.local
.claude/settings.local.json
The .worktreeinclude file itself can be committed.
The files it references usually aren’t.
When I create a worktree, my tooling reads this file and copies the matching files from the main checkout into the new workspace.
The logic lives in copy-worktree-includes.sh.
This small convention made a surprisingly big difference. A newly created worktree is no longer just a Git checkout. It’s much closer to a workspace that is ready for an agent to use.
I only put files there that I actually want copied. In particular, I treat .worktreeinclude as trusted project configuration because it may point to local files containing credentials or other sensitive values.
I don’t create worktrees manually anymore
Once I started using worktrees regularly, I didn’t want to remember the exact Git commands every time.
So I added create-worktree.sh.
My normal workflow is now closer to:
./tools/scripts/create-worktree.sh ../worktrees/fix-auth
The script creates the worktree and its branch, then copies the files listed in .worktreeinclude.
I can also choose a specific starting point when I need one:
./tools/scripts/create-worktree.sh ../worktrees/fix-auth main
The name of the worktree directory becomes the branch name, which is another small convention that keeps things predictable.
So:
worktrees/fix-auth
means I’m working on:
fix-auth
There isn’t much abstraction here. The scripts are intentionally small. They just remove enough friction that creating another workspace becomes cheap.
Cleanup should be cheap too
Creating temporary environments is useful only if deleting them is equally easy.
For that I use delete-worktree.sh.
When the task is done:
./tools/scripts/delete-worktree.sh ../worktrees/fix-auth
The worktree disappears, and normally the corresponding branch is removed as well.
There are cases where I want to keep the branch:
./tools/scripts/delete-worktree.sh ../worktrees/fix-auth --keep-branch
And there is a force option for cases where I intentionally want to discard unfinished work.
Again, none of this is particularly complicated.
That’s the point.
I don’t want worktree management to become another workflow I need to think about.
How this fits my OpenSpec workflow
This is where worktrees became much more useful to me.
With OpenSpec, features naturally move through different phases.
For example:
Feature A
spec → approved → implementation
Feature B
spec → review → changes
Feature C
implementation → review
These phases require very different amounts of attention from me.
During implementation, an agent can often work for a while without much input.
Specification is different. I need to read what is being proposed, clarify requirements, challenge assumptions, and decide what I actually want to build.
So a typical situation may look like this:
project-name-meta/
├── project-name/
└── worktrees/
├── feature-a/ ← agent implementing
└── feature-b/ ← I'm reviewing the spec with another agent
Feature A can keep progressing while most of my attention is on Feature B.
Later the roles may reverse.
This is the part that changed how I think about parallel work with agents.
Parallel doesn’t necessarily mean:
Agent A is coding
Agent B is coding
Agent C is coding
It can mean:
Feature A → autonomous implementation
Feature B → specification, needs my attention
Feature C → waiting for review
The worktrees give each feature a stable workspace while they move independently through those phases.
I don’t need to stash changes, reset files, or switch the whole repository from one feature context to another.
What this looks like day to day
A project might look like this:
project-name-meta/
├── project-name/ ← main workspace
└── worktrees/
├── fix-auth/ ← agent implementing
├── add-export/ ← spec/review with me
└── investigate-sync/ ← agent investigating
Each one has:
- its own branch
- its own filesystem state
- its own terminal
- its own coding-agent session
- the local configuration it needs
The main repository can remain on whatever I’m currently working on.
I don’t need to stash changes because another feature needs a different branch.
I also don’t need to wait for one feature to completely finish before starting to think about the next one.
I don’t parallelize everything
Worktrees make parallel work easier, but they don’t make every task independent.
If two agents are modifying the same core files, I still have a merge problem.
The same applies when one task depends heavily on another unfinished change.
So I try to parallelize work that has reasonably clear boundaries.
Good candidates are things like:
fix an isolated bug
add missing tests
refactor one module
investigate a problem
implement a small independent feature
update documentation
A worse setup would be:
Agent A → redesign the API
Agent B → build a feature against that API at the same time
The filesystem is isolated, but the work isn’t.
A simple rule works well for me:
I parallelize independent work, not interconnected work.
Worktrees became feature environments
Before coding agents, I mostly thought about worktrees as an alternative way to switch between branches.
That wasn’t compelling enough to change my workflow.
Now I think about them differently.
A worktree is a cheap, disposable environment for one feature.
I can create one:
./tools/scripts/create-worktree.sh ../worktrees/fix-auth
move the feature through specification, implementation, and review, then remove the workspace when I’m done:
./tools/scripts/delete-worktree.sh ../worktrees/fix-auth
The scripts themselves are small:
The important part isn’t the scripts themselves.
It’s that they make creating another isolated workspace cheap enough that I actually do it.
Containers are useful when I need runtime isolation.
Worktrees give me something different: development isolation.
For coding agents, and especially for several features moving through different stages at once, that’s often exactly enough.