You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
provision_worktree shields a transient worktree by writing repo-wide, permanent exclude patterns. The patterns name directories that projects legitimately track (.claude/skills, .claude/settings.json), and nothing ever removes them — so long after the run, every new file under those paths is invisible to git add -A in the main checkout.
Observed on bmad-loop 0.9.0, git 2.30.0, macOS.
What happens
worktree_flow.py:286-293 builds the shield set from the CLI profiles:
_worktree_local_exclude (install.py:734) resolves its target with git rev-parse --git-common-dir (install.py:787-793). From a linked worktree that answers with the main repo's.git, so the patterns are appended to the repository-wide .git/info/exclude.
The defaults in adapters/profile.py:50,62 are .claude/settings.json and .claude/skills, so a stock claude profile writes exactly:
/.claude/settings.json
/.claude/skills
Two properties combine badly:
Scope — the file is shared by the main checkout and every worktree, so a shield meant for one unit applies to the user's own working tree.
Lifetime — _worktree_local_exclude is the only writer and has no counterpart remover; gc_run_worktrees reclaims the worktree and leaves the lines behind. isolation back to "none" doesn't matter; the lines outlive the feature that wrote them.
The docstring argues the write is safe because it "does not affect already-tracked files." True, and that is precisely what makes it hard to notice: .claude/skills/ is full of tracked files that keep diffing normally, while every newly created sibling silently disappears from git status and git add -A.
Impact in my repo
/.claude/skills sat in .git/info/exclude across two BMAD-METHOD installer upgrades. Both upgrade commits captured only modifications to files that were already tracked; 51 new files were never staged, including three whole skills (bmad-review, bmad-editorial-review, bmad-deep-recon).
The merged result was an internally inconsistent repo: _bmad/_config/bmad-help.csv and files-manifest.csv referenced bmad-review, and the committed deprecation stubs (bmad-editorial-review-prose, bmad-review-adversarial-general, …) forwarded to it — but a fresh clone had no such skill. Nothing in the diff of either PR could reveal why, since .git/info/exclude is unversioned and unreviewable by construction.
.bmad-loop/runs/*/state.json records "isolation": "worktree" on 17 runs here, so this is the ordinary path, not an exotic configuration.
Two things I verified before filing
A per-worktree info/exclude does not exist. Writing /marker.txt into .git/worktrees/<id>/info/exclude has no effect — git only reads $GIT_COMMON_DIR/info/exclude:
So the choice of the common dir is not the bug — it is the only exclude file git honors. Any fix that just redirects the write to the worktree's private git dir silently does nothing.
core.excludesFile scoped per-worktree does work. With extensions.worktreeConfig:
Scope the shield to the worktree via git config --worktree core.excludesFile <path> pointing at a private file, gated on extensions.worktreeConfig. Correct scope and correct lifetime (the config dies with the worktree). Caveats worth weighing: extensions.worktreeConfig is repo-wide state, it moves an existing core.excludesFile into the main worktree's config on first enable, and it would clobber a user's own per-worktree core.excludesFile — so it needs the same read-modify-write care the current helper already has.
If the shared file stays, make the block reversible. Fence it with markers and delete it on reclaim, e.g.
# bmad-loop <run-id> — removed on reclaim
/.claude/skills
# end bmad-loop
Cheap mitigation, independent of the above: skip any pattern whose path already contains tracked files (git ls-files --error-unmatch <path>). For a tracked path the exclude cannot shield the files it names anyway — those are tracked, so git add -A stages them regardless — and its only real effect is hiding their untracked siblings. Dropping those patterns costs nothing and removes the surprising case entirely.
Happy to send a PR for whichever direction you prefer.
provision_worktreeshields a transient worktree by writing repo-wide, permanent exclude patterns. The patterns name directories that projects legitimately track (.claude/skills,.claude/settings.json), and nothing ever removes them — so long after the run, every new file under those paths is invisible togit add -Ain the main checkout.Observed on bmad-loop 0.9.0, git 2.30.0, macOS.
What happens
worktree_flow.py:286-293builds the shield set from the CLI profiles:_worktree_local_exclude(install.py:734) resolves its target withgit rev-parse --git-common-dir(install.py:787-793). From a linked worktree that answers with the main repo's.git, so the patterns are appended to the repository-wide.git/info/exclude.The defaults in
adapters/profile.py:50,62are.claude/settings.jsonand.claude/skills, so a stockclaudeprofile writes exactly:Two properties combine badly:
_worktree_local_excludeis the only writer and has no counterpart remover;gc_run_worktreesreclaims the worktree and leaves the lines behind.isolationback to"none"doesn't matter; the lines outlive the feature that wrote them.The docstring argues the write is safe because it "does not affect already-tracked files." True, and that is precisely what makes it hard to notice:
.claude/skills/is full of tracked files that keep diffing normally, while every newly created sibling silently disappears fromgit statusandgit add -A.Impact in my repo
/.claude/skillssat in.git/info/excludeacross two BMAD-METHOD installer upgrades. Both upgrade commits captured only modifications to files that were already tracked; 51 new files were never staged, including three whole skills (bmad-review,bmad-editorial-review,bmad-deep-recon).The merged result was an internally inconsistent repo:
_bmad/_config/bmad-help.csvandfiles-manifest.csvreferencedbmad-review, and the committed deprecation stubs (bmad-editorial-review-prose,bmad-review-adversarial-general, …) forwarded to it — but a fresh clone had no such skill. Nothing in the diff of either PR could reveal why, since.git/info/excludeis unversioned and unreviewable by construction..bmad-loop/runs/*/state.jsonrecords"isolation": "worktree"on 17 runs here, so this is the ordinary path, not an exotic configuration.Two things I verified before filing
A per-worktree
info/excludedoes not exist. Writing/marker.txtinto.git/worktrees/<id>/info/excludehas no effect — git only reads$GIT_COMMON_DIR/info/exclude:So the choice of the common dir is not the bug — it is the only exclude file git honors. Any fix that just redirects the write to the worktree's private git dir silently does nothing.
core.excludesFilescoped per-worktree does work. Withextensions.worktreeConfig:Suggested fixes, roughly in order of preference
Scope the shield to the worktree via
git config --worktree core.excludesFile <path>pointing at a private file, gated onextensions.worktreeConfig. Correct scope and correct lifetime (the config dies with the worktree). Caveats worth weighing:extensions.worktreeConfigis repo-wide state, it moves an existingcore.excludesFileinto the main worktree's config on first enable, and it would clobber a user's own per-worktreecore.excludesFile— so it needs the same read-modify-write care the current helper already has.If the shared file stays, make the block reversible. Fence it with markers and delete it on reclaim, e.g.
Claude Code writes its own runtime state into the same file behind a
# claude-code-runtimemarker, so the convention is already established there. This also gives Exclude helper's read-modify-write is unserialized: concurrent worktree provisioning silently loses patterns #381's interleaving a smaller blast radius.Cheap mitigation, independent of the above: skip any pattern whose path already contains tracked files (
git ls-files --error-unmatch <path>). For a tracked path the exclude cannot shield the files it names anyway — those are tracked, sogit add -Astages them regardless — and its only real effect is hiding their untracked siblings. Dropping those patterns costs nothing and removes the surprising case entirely.Happy to send a PR for whichever direction you prefer.