What
provision_worktree's seed_files loop skips a worktree_seed entry silently when the
source resolves outside repo_root:
src = (repo_root / rel).resolve()
dst = (worktree / rel).resolve()
if not src.is_relative_to(repo_root) or not dst.is_relative_to(worktree):
continue # <- silent
if not src.exists():
continue # <- silent
if dst.exists():
skipped.append(str(rel)) # <- the ONLY reported outcome
So from the caller's side, a seed entry whose source is a symlink pointing outside the repo is
indistinguishable from one that applied. The engine journals worktree-seed-skipped from the
returned list, so nothing is journalled either.
Why it matters
Same shape as the two cases already fixed on #406:
_bmad/scripts + _bmad/config.toml — covered by _bmad_scripts_seed_incomplete /
_central_config_seed_incomplete, which re-ask the question of the result.
- the
BASE_SKILLS copy — covered by base_skills_seed_incomplete (897988f).
seed_files is the remaining leg. A shared/symlinked config (an MCP config, a CLI settings file
outside the repo) is exactly the wiring that triggers it, and the seeded configs are the things
an isolated session needs to reach its MCP server.
Lower severity than the two fixed cases: a missing seeded config degrades a session rather than
guaranteeing a result-less Stop, and worktree_seed is opt-in. That is why it was left out of
the 0.9.1 hotfix rather than fixed alongside.
Also worth a look
The seed_globs loop has the same containment continue. Its silence is documented — a
plugin's glob is expected to hit paths the checkout already carries — but that justification is
written for the dst.exists() case and the containment failure rides along in the same silence
with no separate rationale.
Suggested shape
Report the containment leg the way the other three now do: re-probe the result rather than
accumulating inside the loop, so the report survives any future path that drops a file. Whether
it should escalate (as the skills/renderer legs do) or stay a journal line is the open design
question — it depends on whether a seed entry is treated as required.
Provenance
Found while triaging the Codex review on #406 (PR #406 comment thread). Not introduced by #405 —
the guard predates it.
What
provision_worktree'sseed_filesloop skips aworktree_seedentry silently when thesource resolves outside
repo_root:So from the caller's side, a seed entry whose source is a symlink pointing outside the repo is
indistinguishable from one that applied. The engine journals
worktree-seed-skippedfrom thereturned list, so nothing is journalled either.
Why it matters
Same shape as the two cases already fixed on #406:
_bmad/scripts+_bmad/config.toml— covered by_bmad_scripts_seed_incomplete/_central_config_seed_incomplete, which re-ask the question of the result.BASE_SKILLScopy — covered bybase_skills_seed_incomplete(897988f).seed_filesis the remaining leg. A shared/symlinked config (an MCP config, a CLI settings fileoutside the repo) is exactly the wiring that triggers it, and the seeded configs are the things
an isolated session needs to reach its MCP server.
Lower severity than the two fixed cases: a missing seeded config degrades a session rather than
guaranteeing a result-less Stop, and
worktree_seedis opt-in. That is why it was left out ofthe 0.9.1 hotfix rather than fixed alongside.
Also worth a look
The
seed_globsloop has the same containmentcontinue. Its silence is documented — aplugin's glob is expected to hit paths the checkout already carries — but that justification is
written for the
dst.exists()case and the containment failure rides along in the same silencewith no separate rationale.
Suggested shape
Report the containment leg the way the other three now do: re-probe the result rather than
accumulating inside the loop, so the report survives any future path that drops a file. Whether
it should escalate (as the skills/renderer legs do) or stay a journal line is the open design
question — it depends on whether a seed entry is treated as required.
Provenance
Found while triaging the Codex review on #406 (PR #406 comment thread). Not introduced by #405 —
the guard predates it.