Description
Summary
With scm.isolation = "worktree" and the Codex adapter, worktree provisioning adds /.codex/hooks.json to the repository's shared .git/info/exclude, even when .codex/hooks.json is already tracked and was not modified during provisioning.
This makes the tracked hook configuration appear ignored and can break repository-hygiene or pre-commit checks.
Environment
- bmad-loop: 0.9.0
- Installed source revision:
232f3f7159004e960e786a6568ebe79daecf388d
- Upstream
main also affected at: ad22f7966b0d0626bea3a22f0617f55c875b7810
- Adapter: Codex
- Git: 2.54.0
- OS: Linux
- SCM policy:
isolation = "worktree"
branch_per = "story"
seed_adapter_defaults = true
Steps to reproduce
Steps to reproduce
-
Create a Git repository with a tracked .codex/hooks.json:
git add .codex/hooks.json
git commit -m "Track Codex hook configuration"Confirm no tracked files are ignored:
git ls-files -ci --exclude-standard
Configure bmad-loop to use the Codex adapter and worktree isolation.
Start a run that provisions an isolated story worktree.
Inspect the common Git exclude file:
git check-ignore --no-index -v .codex/hooks.json
Check for tracked-and-ignored files again:
git ls-files -ci --exclude-standard
Expected behavior
Expected behavior
Worktree provisioning should:
Never make an already-tracked file ignored.
Avoid changing ignore behavior for the main checkout or unrelated worktrees.
Exclude only untracked files actually created by provisioning.
Remove any provisioning-owned metadata during worktree/run cleanup.
Actual behavior
Actual behavior
After provisioning:
.git/info/exclude:...:/.codex/hooks.json .codex/hooks.json
and:
.codex/hooks.json
is returned by git ls-files -ci --exclude-standard.
provision_worktree() unconditionally adds every non-hookless profile's hook configuration to the exclusion set:
patterns |= {
f"/{p.hooks.config_path}"
for p in profiles
if not p.hookless
}
_worktree_local_exclude() then resolves git rev-parse --git-common-dir and writes to:
common_dir / "info" / "exclude"
For a linked worktree, that is the main repository's shared Git directory. The exclusion therefore affects the main checkout and every linked worktree; it is not worktree-local.
Deleting the run or its worktree does not remove the shared exclusion entry.
Impact
Repositories that reject tracked-and-ignored files can pass preflight and then begin failing immediately after bmad-loop provisions its story worktree.
In our run, this blocked the repository commit/review gate and resulted in a CRITICAL review escalation. The implementation remained isolated and was never merged, but the shared .git/info/exclude residue required manual cleanup.
Suggested fix
Before adding a path to the exclusion set, check whether it is tracked:
git ls-files --error-unmatch --
Never exclude tracked paths.
Track which hook or skill paths were actually created or modified during provisioning; do not add every configured path unconditionally.
Do not describe or implement /info/exclude as worktree-local. Use genuinely worktree-scoped state where possible, or explicitly manage shared entries with ownership and cleanup.
Add a regression test that:
Starts with a tracked .codex/hooks.json.
Provisions a linked worktree.
Asserts git ls-files -ci --exclude-standard remains empty in both the main checkout and linked worktree.
Deletes the run/worktree and verifies no exclude residue remains.
Temporary workaround
After worktree provisioning, remove /.codex/hooks.json from the common repository's .git/info/exclude.
Removing it before the run is insufficient because provisioning adds it again.
The upstream issue tracker currently reports
Screenshots
No response
Which area is this for?
Orchestrator / control loop
bmad-loop Version
0.9.0
Which coding CLI are you using?
Codex (codex)
Operating System
Linux
Relevant log output
Diagnostic dump (bmad-loop diagnose)
PAUSED: CRITICAL escalation from review session: ### Status
- Final status:
blocked.
- Blocking condition: the required scoped checkpoint cannot commit because
bun run check:repo-hygiene rejects the pre-existing tracked-and-ignored .codex/hooks.json; bypassing the repository hook or changing that out-of-scope file was not authorized.
- Secret scan: PASS for the five scoped review files.
Confirm
Description
Summary
With
scm.isolation = "worktree"and the Codex adapter, worktree provisioning adds/.codex/hooks.jsonto the repository's shared.git/info/exclude, even when.codex/hooks.jsonis already tracked and was not modified during provisioning.This makes the tracked hook configuration appear ignored and can break repository-hygiene or pre-commit checks.
Environment
232f3f7159004e960e786a6568ebe79daecf388dmainalso affected at:ad22f7966b0d0626bea3a22f0617f55c875b7810isolation = "worktree"branch_per = "story"seed_adapter_defaults = trueSteps to reproduce
Steps to reproduce
Create a Git repository with a tracked
.codex/hooks.json:git add .codex/hooks.json git commit -m "Track Codex hook configuration"Confirm no tracked files are ignored:git ls-files -ci --exclude-standard
Configure bmad-loop to use the Codex adapter and worktree isolation.
Start a run that provisions an isolated story worktree.
Inspect the common Git exclude file:
git check-ignore --no-index -v .codex/hooks.json
Check for tracked-and-ignored files again:
git ls-files -ci --exclude-standard
Expected behavior
Expected behavior
Worktree provisioning should:
Never make an already-tracked file ignored.
Avoid changing ignore behavior for the main checkout or unrelated worktrees.
Exclude only untracked files actually created by provisioning.
Remove any provisioning-owned metadata during worktree/run cleanup.
Actual behavior
Actual behavior
After provisioning:
.git/info/exclude:...:/.codex/hooks.json .codex/hooks.json
and:
.codex/hooks.json
is returned by git ls-files -ci --exclude-standard.
provision_worktree() unconditionally adds every non-hookless profile's hook configuration to the exclusion set:
patterns |= {
f"/{p.hooks.config_path}"
for p in profiles
if not p.hookless
}
_worktree_local_exclude() then resolves git rev-parse --git-common-dir and writes to:
common_dir / "info" / "exclude"
For a linked worktree, that is the main repository's shared Git directory. The exclusion therefore affects the main checkout and every linked worktree; it is not worktree-local.
Deleting the run or its worktree does not remove the shared exclusion entry.
Impact
Repositories that reject tracked-and-ignored files can pass preflight and then begin failing immediately after bmad-loop provisions its story worktree.
In our run, this blocked the repository commit/review gate and resulted in a CRITICAL review escalation. The implementation remained isolated and was never merged, but the shared .git/info/exclude residue required manual cleanup.
Suggested fix
Before adding a path to the exclusion set, check whether it is tracked:
git ls-files --error-unmatch --
Never exclude tracked paths.
Track which hook or skill paths were actually created or modified during provisioning; do not add every configured path unconditionally.
Do not describe or implement /info/exclude as worktree-local. Use genuinely worktree-scoped state where possible, or explicitly manage shared entries with ownership and cleanup.
Add a regression test that:
Starts with a tracked .codex/hooks.json.
Provisions a linked worktree.
Asserts git ls-files -ci --exclude-standard remains empty in both the main checkout and linked worktree.
Deletes the run/worktree and verifies no exclude residue remains.
Temporary workaround
After worktree provisioning, remove /.codex/hooks.json from the common repository's .git/info/exclude.
Removing it before the run is insufficient because provisioning adds it again.
The upstream issue tracker currently reports
Screenshots
No response
Which area is this for?
Orchestrator / control loop
bmad-loop Version
0.9.0
Which coding CLI are you using?
Codex (codex)
Operating System
Linux
Relevant log output
Diagnostic dump (bmad-loop diagnose)
PAUSED: CRITICAL escalation from review session: ### Status
blocked.bun run check:repo-hygienerejects the pre-existing tracked-and-ignored.codex/hooks.json; bypassing the repository hook or changing that out-of-scope file was not authorized.Confirm