Running long builds in a separate worktree Jump to heading
A long build in the directory you are working in creates a rule nobody can follow: do not edit anything for ten minutes. Editing a file mid-build produces a mixture of old and new inputs and a result that is neither, and the failure usually surfaces later as something inexplicable. Pinning the build to its own worktree at a fixed commit removes the rule entirely β the build reads a tree that cannot change while it runs. This recipe sets that up, within Git worktrees and parallel development.
When to use this approach Jump to heading
- Your full build takes long enough that waiting is not an option.
- You have been caught editing during a build and debugging the result.
- You want to build a release candidate while continuing on the next change.
- Your build system writes into the repository and you want that traffic elsewhere.
- If your build takes fifteen seconds, this is overhead; run it where you are.
Step 1 β Create a build worktree pinned to a commit Jump to heading
Detached and pinned is the point: nothing about this directory should move unless you move it.
git worktree add --detach ../app-build HEAD # Verification: pinned to a specific commit, claiming no branch
git -C ../app-build rev-parse --short HEAD
git -C ../app-build symbolic-ref -q HEAD || echo "detached, as intended" # Advance it deliberately when you want the build to see newer work
git -C ../app-build fetch origin && git -C ../app-build checkout --detach "$(git rev-parse HEAD)" Step 2 β Give it its own output directory Jump to heading
Two worktrees writing to the same build cache is the second way this goes wrong, and it is quieter than the first.
# Explicit output path per worktree
export BUILD_DIR="$PWD/.build"
npm run build -- --outDir "$BUILD_DIR"
# Tools that key their cache on a project name rather than a path need telling
export TURBO_CACHE_DIR="$PWD/.turbo"
export CARGO_TARGET_DIR="$PWD/target" # Verification: the two worktrees write to different places
ls -d ../app/.build ../app-build/.build 2>/dev/null Cache directories that default to a location under the userβs home β shared across every checkout β are the usual culprit. Checking this once per tool is worth more than debugging a corrupted incremental build later.
Step 3 β Keep dependency state consistent Jump to heading
The build worktree needs its own dependency tree, and it must match the commit it is pinned to rather than the one you are editing.
(cd ../app-build && npm ci) # exactly what the pinned lockfile says # Verification: the two worktrees may legitimately differ β confirm which is which
sha256sum package-lock.json ../app-build/package-lock.json SAFETY WARNING β do not symlink the dependency directory into a build worktree. The build worktree is pinned to an older commit precisely so that it does not change, and a symlink makes its dependencies change whenever you install in the directory you are editing. The build then links a tree assembled from two different lockfiles, and the artefact it produces corresponds to nothing you can reproduce.
Step 4 β Run the build from the pinned tree Jump to heading
# One command that records exactly what was built
commit=$(git -C ../app-build rev-parse HEAD)
(cd ../app-build && npm run build) && echo "built $commit" # Stamp the artefact so it can be traced back later
echo "$commit" > ../app-build/.build/COMMIT # Verification: the stamp matches the worktree's HEAD
diff <(git -C ../app-build rev-parse HEAD) ../app-build/.build/COMMIT && echo "traceable" Stamping is a small habit with a large return: when an artefact behaves oddly a week later, the alternative to a recorded commit is guessing. The same reasoning scales to releases in linking a container image to its commit.
Step 5 β Reuse it rather than recreating it Jump to heading
Creating a worktree is cheap; installing dependencies into it is not. Keep one and re-point it.
# Move the build worktree to a new commit without recreating anything
git -C ../app-build checkout --detach "$(git rev-parse HEAD)"
# Reinstall only when the lockfile actually differs
if ! diff -q package-lock.json ../app-build/package-lock.json >/dev/null; then
(cd ../app-build && npm ci)
fi # Verification: dependency install is skipped when nothing changed
echo "lockfiles match: $(diff -q package-lock.json ../app-build/package-lock.json >/dev/null && echo yes || echo no)" Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why not just commit before building? Jump to heading
Committing is a fine discipline and does not solve the problem: you can still edit during the build, and the build still reads the working tree rather than the commit. The pinned worktree makes the guarantee structural instead of relying on restraint.
Does this help with continuous integration? Jump to heading
It is the same idea CI applies by construction β build a specific commit in a clean directory β brought to a developer machine. If your local builds and your pipeline builds disagree, a pinned worktree removes one of the likeliest explanations before you go looking for others.
What about builds that write into the source tree? Jump to heading
Those are exactly the builds this helps most, because the writes land in the pinned worktree rather than in the files you are editing. Add the output paths to .gitignore so the pinned worktree stays clean and git status there remains meaningful.
Related Jump to heading
- Git Worktrees & Parallel Development β the parent topic and the sharing model.
- Scripting a Worktree-per-Ticket Workflow β wrapping this into one command.
- Cleaning Up Stale Worktrees Safely β removing build worktrees without losing work.