TL;DR
Git worktrees give you multiple working directories, but they still share one underlying repository. When multiple processes run write commands at the same time, they collide on Git lock files (.git/index.lock, ref locks, etc.).
Treat Git as a single-writer system:
- serialize Git writes with a mutex/lock wrapper, or
- give each worker its own clone (true isolation at the cost of disk/clone time).
Why worktrees + concurrent processes fail
Worktrees are lightweight because they share the .git/ directory with the main worktree. That shared directory contains:
- the index,
- refs, and
- objects packs.
Git protects these shared structures with lockfiles during updates. If two processes try to update at once, you get errors like:
fatal: Unable to create '.git/index.lock',Another git process seems to be running in this repository,cannot lock ref 'refs/heads/feature-x',- or inconsistent branch/remote-tracking state.
Symptoms
If any of these are familiar, you’re dealing with contention:
.git/index.lockexists and Git refuses to proceed- random
git commitfailures - new branches “mysteriously” missing or being overwritten
git pullsays “Already up to date” while you’re clearly behind (because fetch/pull didn’t happen)
What’s safe vs unsafe
Generally unsafe / needs serialization
These commands write to index/refs/objects and should not run concurrently across worktrees:
git add,git commitgit checkout(when it updates the working tree/index)git merge,git rebase,git resetgit fetch,git pull,git pushgit gc,git repack
Generally safe
Read-only operations (usually safe to run concurrently):
git diff,git log,git showgit status(usually read-only, though don’t rely on it to update state)
Pattern 1: serialize Git writes with a mutex
Create a small wrapper script and point your agents at it.
# git-safe
set -euo pipefail
LOCK_FILE="${HOME}/.cache/git-safe/$(pwd | shasum | cut -d' ' -f1).lock"
mkdir -p "$(dirname "$LOCK_FILE")"
(
flock -x 200
git "$@"
) 200>"$LOCK_FILE"
Usage:
git-safe fetch origin
# ...
git-safe add .
git-safe commit -m "Update"
git-safe push
This keeps your agents fully parallel inside their worktrees, while letting Git writes happen one at a time.
Pattern 2: a single “sync” process
Instead of letting every agent do git fetch / git pull, run a single sync process that:
- periodically runs
git-safe fetch origin, - optionally updates tracking branches,
- optionally pulls the branch each worktree tracks.
Everyone else works locally, using the repo state that sync refreshed.
Pattern 3: clones per worker (when you truly need parallel Git writes)
If you want full isolation, don’t use shared .git/ at all.
git clone --filter=blob:none --no-checkout <URL> worker-1
git clone --filter=blob:none --no-checkout <URL> worker-2
If disk usage is a concern, keep the shared/central repository and use shallow clones or partial clone filters.
Pattern 4: bootstrap worktrees so the agent doesn’t choke on local state
Worktrees don’t share:
.env,- build artifacts,
node_modules/ caches,- local databases.
When you create a worktree, immediately hydrate it:
cp ../main/.env .env
pnpm install
Whatever your stack requires, codify it in a worktree-init script and run it every time.
Recovering from a stale lock
If you hit lock errors:
- Check for active Git processes (don’t delete locks while another Git command is running).
- If it’s truly stuck and no Git processes remain, delete the lock file and rerun.
rm -f .git/index.lock
Helpful commands
List worktrees:
git worktree list
Create a worktree on a branch:
git worktree add ../wt-feature feature/my-feature
Remove a worktree once done:
git worktree remove ../wt-feature
Closing thought
Worktrees are great for multi-branch workflows and agents, but they share one .git/. Git is fine with multiple read-only operations, but you must serialize writes or isolate them with clones. That’s the entire problem, and the entire fix.