A hotfix need not displace unfinished feature work from its directory. A linked worktree can start from a chosen release commit while the original directory retains its staged changes, unstaged edits, and untracked files. The workflow succeeds when the fix is tested on the correct baseline and the original work remains intact. Merely creating a second folder does not establish either result.
Choose the baseline before opening another directory
Inspect the current worktree and identify the exact release revision needing the fix. A convenient branch name is not necessarily the deployed revision. If a remote-tracking reference must be refreshed, fetch the intended remote and then record the resolved commit. Fetching updates shared repository references; it does not modify only the directory where the command was entered.
Create the new worktree with an explicit path, branch name, and start point. Using a new hotfix branch avoids competing with a branch already checked out elsewhere. For inspection without intended commits, a detached worktree may be sufficient. Do not use branch-resetting or force options merely because the proposed name already exists; inspect the existing worktree and reference first.
Creation checks out committed content from the selected start point. It does not copy the original directory’s uncommitted feature changes or untracked notes. That separation is useful for reproducing a release problem without accidental fixes or dependencies from the feature workspace. It also means editor buffers and local configuration files do not automatically become available in the new directory.
Reproduce and verify in the actual hotfix tree
Confirm the new HEAD and run the targeted reproduction before changing code. After the repair, run that same check and the relevant broader tests against the hotfix directory. A passing command run from the feature directory is evidence about the wrong checkout. Keep test output associated with the exact commit, directory, and configuration used.
Build outputs placed inside each directory are separate files, but external state may still collide. Two servers can bind the same port, two test suites can write the same database, and shared caches may need correctly keyed entries. A worktree is not a container, virtual machine, or security boundary. Review what build commands execute and which resources they reach before running unfamiliar branch code.
Stage only the intended fix and inspect its diff before committing. The new commit is visible through the shared object database, but the original feature branch and its index need not move. Verify those unchanged states explicitly when preservation matters. Local availability also does not publish the branch or deploy the repair; distribution remains a separate authorized step.
When it breaks
The directory name can suggest a release while its HEAD actually points elsewhere. Check the resolved revision instead of trusting a folder label. Another common mistake is treating shared repository configuration as worktree-private configuration. A setting changed for a hotfix can affect other worktrees, even though their checked-out files remain untouched.
When finished, inspect the linked worktree’s status, untracked files, and valuable ignored outputs. Ordinary status omits ignored files; a clean tracked diff is not proof that the directory contains nothing worth saving. Keep required logs or artifacts outside the directory before removal. Stop processes still using it, because a clean Git status says nothing about running servers or open files.
Use normal git worktree remove on the confirmed path. It refuses ordinary dirty or untracked work rather than silently treating it as disposable. Investigate that refusal instead of routinely adding force. The command removes the linked checkout and its registration, not its branch. Prune is for stale administrative records whose directories are missing, not a command to delete active workspaces.
Check it quickly
Run this with Python 3 and Git 2.28 or newer. The temporary repository excludes personal Git configuration and inherited Git routing, disables hooks and signing, and has no remote. The feature directory deliberately contains three kinds of unfinished work. The hotfix test first fails, then passes after a one-line repair.
import os
import subprocess
import sys
import tempfile
from pathlib import Path
env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull)
with tempfile.TemporaryDirectory(prefix="git-hotfix-demo-") as directory:
root = Path(directory)
feature, hotfix = root / "feature", root / "hotfix"
feature.mkdir()
def git(tree, *args, check=True):
return subprocess.run(
["git", "-c", "user.name=Hotfix Example",
"-c", "user.email=hotfix@example.invalid",
"-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", *args],
cwd=tree, env=env, text=True, capture_output=True, check=check,
)
def test():
return subprocess.run([sys.executable, "-B", "-c",
"from calc import double; assert double(4) == 8"],
cwd=hotfix, text=True, capture_output=True)
git(feature, "init", "-q", "-b", "feature")
(feature / "calc.py").write_text("def double(n):\n return n * 2 + 1\n")
(feature / "draft.txt").write_text("base\n")
git(feature, "add", "calc.py", "draft.txt")
git(feature, "commit", "-qm", "release baseline")
baseline = git(feature, "rev-parse", "HEAD").stdout.strip()
(feature / "draft.txt").write_text("staged\n")
git(feature, "add", "draft.txt")
(feature / "draft.txt").write_text("unstaged\n")
(feature / "idea.txt").write_text("untracked idea\n")
git(feature, "worktree", "add", "-qb", "hotfix", str(hotfix), baseline)
print("clean-baseline:", git(hotfix, "rev-parse", "HEAD").stdout.strip() == baseline
and (hotfix / "draft.txt").read_text() == "base\n"
and not (hotfix / "idea.txt").exists())
broken = test()
print("regression-reproduced:", broken.returncode != 0 and "AssertionError" in broken.stderr)
(hotfix / "calc.py").write_text("def double(n):\n return n * 2\n")
fixed = test()
assert fixed.returncode == 0 and not fixed.stderr, fixed
git(hotfix, "add", "calc.py")
git(hotfix, "commit", "-qm", "fix double")
fixed_commit = git(hotfix, "rev-parse", "HEAD").stdout.strip()
print("feature-preserved:", git(feature, "rev-parse", "HEAD").stdout.strip() == baseline
and git(feature, "symbolic-ref", "--short", "HEAD").stdout.strip() == "feature"
and git(feature, "show", ":draft.txt").stdout == "staged\n"
and (feature / "draft.txt").read_text() == "unstaged\n"
and (feature / "idea.txt").read_text() == "untracked idea\n")
artifact = hotfix / "review-notes.txt"
artifact.write_text("reviewed\n")
refused = git(feature, "worktree", "remove", str(hotfix), check=False)
print("dirty-removal-refused:", refused.returncode != 0 and artifact.exists())
saved = root / "saved-review.txt"
artifact.rename(saved)
git(feature, "worktree", "remove", str(hotfix))
print("checkout-removed-branch-kept:", not hotfix.exists()
and git(feature, "rev-parse", "hotfix").stdout.strip() == fixed_commit)
print("review-notes-preserved:", saved.read_text() == "reviewed\n")
All six observations should print True. The hotfix starts from committed baseline content, reproduces the bug, and leaves every original feature state unchanged. An untracked review note prevents removal until it is moved outside the linked checkout. Normal removal then succeeds while the hotfix branch and saved note remain. The outer temporary-directory cleanup intentionally discards all demonstration data only after these checks.
Sources
- Git worktree reference, explicit creation, checkout safeguards, removal, and pruning, checked 2026-09-07.
- Git status reference, staged versus working-tree changes and untracked or ignored files, checked 2026-09-07.