CB-628: a tracked file neutralized by the worktree overlay can never be edited by a worker, and nothing says so #134

Closed
opened 2026-08-22 21:53:38 +02:00 by ltms · 3 comments
Owner

Found while running CB-622 (#126). A worker did everything right and still could not make a change that was in its brief.

What happened

CB-622 Unit C was told to rename the MCP mount key bridged -> fleetd in opencode.json. It reported back:

opencode.json was on your Part 2 list but contains no bridged mount entry (only a $schema key), so there was nothing to rename and I left it byte-identical.

That report is accurate about what the worker could see, and the worker was right to flag it. But the repo's tracked opencode.json is 30+ lines and does carry the mount key.

The cause is GitWorktrees.isolateToolSurface. opencode.json is in WORKTREE_HOSTILE_CONFIGS, so every provisioned worktree gets a neutral stub instead of the real file, and the stub is flagged --skip-worktree. Measured:

worktree 2bc575-1 (claude-code)   3 bytes   {}          <- the neutral stub
worktree 50d8f2-3 (opencode)     51 bytes   rewritten   <- opencode's own minimal JSONC
worktree da7d01-2 (opencode)     51 bytes   rewritten
repo HEAD                       30+ lines   the real file, with the mount key
$ git -C <worktree> ls-files -v
S .mcp.json
S opencode.json

The neutralization itself is correct and must stay. Its reason is documented in GitWorktrees and it is a good one: the tracked opencode.json mounts gitea and context7 with the primary's credentials, and a member must never hold those. This ticket does not propose weakening it.

The defect

The set of files a worker cannot edit is invisible to the worker, to the lead writing the brief, and to review.

Today that set has two members, and they overlap only partly:

how it is hidden files who can edit
gitignored, so never in a worktree bridged/bridged.yaml lead only
neutralized by the overlay, --skip-worktree .mcp.json, opencode.json, .autoenv lead only

The gitignored case is already known and written down. The overlay case is not written down anywhere, and it is worse, because the file is present in the worktree with plausible-looking content. A worker does not see an absent file and ask; it sees a real file, reads it, and reports a truthful conclusion that is wrong about the repo.

This is the same family as CB-608 and CB-610: a check or a view that is narrower than everyone believes it to be, with nothing saying so.

Scope

  1. Make the daemon say it. When a worktree is provisioned, log one line naming every file that was neutralized, at the path the worker will see. The lead reads the log; today nothing reports this at all.
  2. Make the worker able to ask. Put the list where a member can find it — a generated .bridged/neutralized-files in the worktree, or a line in the member charter. A worker that reads "this file was replaced; the repo's version differs; it is not yours to edit" will say so instead of concluding the key does not exist.
  3. Write it down for the lead. Add the overlay set to the same place the gitignored-config rule lives, so "can a worker even see this file?" is answerable before a brief is written, not after a PR comes back.

Not in scope

Do not change what is neutralized, and do not weaken the credential boundary. This ticket is about visibility only.

Acceptance criteria

  • Provisioning a worktree logs the neutralized file list once, with paths.
  • A member can discover the list from inside its own worktree, without asking the lead.
  • A brief that names a neutralized file gets a report saying "this file is neutralized in my worktree; the repo version differs" rather than a wrong conclusion about the repo's content.
  • The credential boundary is unchanged: .mcp.json, opencode.json and .autoenv are still neutralized in every worktree.

Fixed already, separately

The opencode.json mount rename itself is done on lead/cb-622d-opencode-mount (commit 2bce7e3), by hand, by the lead. This ticket is about the blind spot, not that one file.

Found while running CB-622 (#126). A worker did everything right and still could not make a change that was in its brief. ## What happened CB-622 Unit C was told to rename the MCP mount key `bridged` -> `fleetd` in `opencode.json`. It reported back: > `opencode.json` was on your Part 2 list but contains no `bridged` mount entry (only a `$schema` key), so there was nothing to rename and I left it byte-identical. That report is accurate about what the worker could see, and the worker was right to flag it. But the repo's tracked `opencode.json` is 30+ lines and **does** carry the mount key. The cause is `GitWorktrees.isolateToolSurface`. `opencode.json` is in `WORKTREE_HOSTILE_CONFIGS`, so every provisioned worktree gets a neutral stub instead of the real file, and the stub is flagged `--skip-worktree`. Measured: ``` worktree 2bc575-1 (claude-code) 3 bytes {} <- the neutral stub worktree 50d8f2-3 (opencode) 51 bytes rewritten <- opencode's own minimal JSONC worktree da7d01-2 (opencode) 51 bytes rewritten repo HEAD 30+ lines the real file, with the mount key ``` ``` $ git -C <worktree> ls-files -v S .mcp.json S opencode.json ``` The neutralization itself is correct and must stay. Its reason is documented in `GitWorktrees` and it is a good one: the tracked `opencode.json` mounts gitea and context7 with the **primary's** credentials, and a member must never hold those. This ticket does not propose weakening it. ## The defect **The set of files a worker cannot edit is invisible to the worker, to the lead writing the brief, and to review.** Today that set has two members, and they overlap only partly: | how it is hidden | files | who can edit | |---|---|---| | gitignored, so never in a worktree | `bridged/bridged.yaml` | lead only | | neutralized by the overlay, `--skip-worktree` | `.mcp.json`, `opencode.json`, `.autoenv` | lead only | The gitignored case is already known and written down. The overlay case is not written down anywhere, and it is worse, because the file **is** present in the worktree with plausible-looking content. A worker does not see an absent file and ask; it sees a real file, reads it, and reports a truthful conclusion that is wrong about the repo. This is the same family as CB-608 and CB-610: a check or a view that is narrower than everyone believes it to be, with nothing saying so. ## Scope 1. **Make the daemon say it.** When a worktree is provisioned, log one line naming every file that was neutralized, at the path the worker will see. The lead reads the log; today nothing reports this at all. 2. **Make the worker able to ask.** Put the list where a member can find it — a generated `.bridged/neutralized-files` in the worktree, or a line in the member charter. A worker that reads "this file was replaced; the repo's version differs; it is not yours to edit" will say so instead of concluding the key does not exist. 3. **Write it down for the lead.** Add the overlay set to the same place the gitignored-config rule lives, so "can a worker even see this file?" is answerable before a brief is written, not after a PR comes back. ## Not in scope Do not change what is neutralized, and do not weaken the credential boundary. This ticket is about visibility only. ## Acceptance criteria - Provisioning a worktree logs the neutralized file list once, with paths. - A member can discover the list from inside its own worktree, without asking the lead. - A brief that names a neutralized file gets a report saying "this file is neutralized in my worktree; the repo version differs" rather than a wrong conclusion about the repo's content. - The credential boundary is unchanged: `.mcp.json`, `opencode.json` and `.autoenv` are still neutralized in every worktree. ## Fixed already, separately The `opencode.json` mount rename itself is done on `lead/cb-622d-opencode-mount` (commit `2bce7e3`), by hand, by the lead. This ticket is about the blind spot, not that one file.
ltms added this to the 2.0 — one operation centre, many hosts milestone 2026-08-22 21:53:38 +02:00
Author
Owner

Fixed on main in 0d7b4fb (PR #242).

GitWorktrees.overlayParity now names every file it marks --skip-worktree and states the consequence in the message itself:

parity overlay marked --skip-worktree (cannot be committed from this worktree): .env

The ticket's "and nothing says so" is the part that is now closed. A worker whose edit to that file is silently ignored by git has a line in the log that says why.

No marker file is written into the worktree, and that was a deliberate call: the overlay's own invariant is that a worktree holds exactly the configured overlay set and nothing else, so a marker dropped in there would violate the very thing it documents.

Verified by the lead: 1168 tests, 0 failures. Reverting the new line back to log.debug turns the test red with 0 compile errors, so the fix cannot silently regress to an invisible log level. The test also proves the real effect and not just the message — it rewrites the source file after checkout and asserts git status still reports it unmodified.

This landed together with #148 point 3, because both defects were the same silence in the same method.

Fixed on main in `0d7b4fb` (PR #242). `GitWorktrees.overlayParity` now names every file it marks `--skip-worktree` and states the consequence in the message itself: ``` parity overlay marked --skip-worktree (cannot be committed from this worktree): .env ``` The ticket's "and nothing says so" is the part that is now closed. A worker whose edit to that file is silently ignored by git has a line in the log that says why. **No marker file is written into the worktree**, and that was a deliberate call: the overlay's own invariant is that a worktree holds exactly the configured overlay set and nothing else, so a marker dropped in there would violate the very thing it documents. Verified by the lead: 1168 tests, 0 failures. Reverting the new line back to `log.debug` turns the test red with 0 compile errors, so the fix cannot silently regress to an invisible log level. The test also proves the real effect and not just the message — it rewrites the source file after checkout and asserts `git status` still reports it unmodified. This landed together with #148 point 3, because both defects were the same silence in the same method.
ltms closed this issue 2026-09-03 06:15:09 +02:00
ltms reopened this issue 2026-09-03 06:15:28 +02:00
Author
Owner

Correction — I closed this by mistake, and have reopened it

My previous comment was wrong. I conflated two different methods in GitWorktrees:

Method What it neutralises Ticket
isolateToolSurface (line 430) WORKTREE_HOSTILE_CONFIGS — .mcp.json, opencode.json, .autoenv — replaced with stubs and marked --skip-worktree this one, #134
overlayParity (line 483) copies .env/.envrc from the primary, marks a tracked copy --skip-worktree #148 point 3

0d7b4fb changed only overlayParity. It never touched isolateToolSurface, so the stubbed files this ticket is actually about — the ones that made a worker report that opencode.json had no mount key — are still just as invisible as before.

Ironically I made the exact mistake this ticket documents: I read a plausible-looking thing, concluded it was the thing, and reported a wrong conclusion. The two mechanisms even share the --skip-worktree mark, which is what made them look like one.

What is genuinely still open here

All three scope points, none of which 0d7b4fb addressed:

  1. The daemon must say it — log the neutralised WORKTREE_HOSTILE_CONFIGS list once per provisioning, with the paths the worker will see. isolateToolSurface currently reports nothing.
  2. The worker must be able to ask — the list has to be discoverable from inside the worktree (a generated file, or a charter line), so a member says "this file is neutralised in my worktree, the repo version differs" instead of drawing a wrong conclusion about the repo.
  3. Write it down for the lead — so "can a worker even see this file?" is answerable before a brief is written.

Point 1 is now easy and should copy the shape 0d7b4fb established for the sibling method: name the denominator, name each file, and state the consequence in the message. That work is a useful precedent for this ticket, which is the only real connection between them.

The credential boundary stays exactly as it is. This ticket remains visibility only.

## Correction — I closed this by mistake, and have reopened it My previous comment was wrong. I conflated two different methods in `GitWorktrees`: | Method | What it neutralises | Ticket | |---|---|---| | `isolateToolSurface` (line 430) | `WORKTREE_HOSTILE_CONFIGS` — `.mcp.json`, `opencode.json`, `.autoenv` — replaced with stubs and marked `--skip-worktree` | **this one, #134** | | `overlayParity` (line 483) | copies `.env`/`.envrc` from the primary, marks a *tracked* copy `--skip-worktree` | #148 point 3 | `0d7b4fb` changed **only `overlayParity`**. It never touched `isolateToolSurface`, so the stubbed files this ticket is actually about — the ones that made a worker report that `opencode.json` had no mount key — are still just as invisible as before. Ironically I made the exact mistake this ticket documents: I read a plausible-looking thing, concluded it was the thing, and reported a wrong conclusion. The two mechanisms even share the `--skip-worktree` mark, which is what made them look like one. ### What is genuinely still open here All three scope points, none of which `0d7b4fb` addressed: 1. **The daemon must say it** — log the neutralised `WORKTREE_HOSTILE_CONFIGS` list once per provisioning, with the paths the worker will see. `isolateToolSurface` currently reports nothing. 2. **The worker must be able to ask** — the list has to be discoverable from inside the worktree (a generated file, or a charter line), so a member says "this file is neutralised in my worktree, the repo version differs" instead of drawing a wrong conclusion about the repo. 3. **Write it down for the lead** — so "can a worker even see this file?" is answerable *before* a brief is written. Point 1 is now easy and should copy the shape `0d7b4fb` established for the sibling method: name the denominator, name each file, and state the consequence in the message. That work is a useful precedent for this ticket, which is the only real connection between them. The credential boundary stays exactly as it is. This ticket remains visibility only.
Author
Owner

All three points are done. Merged to main in 205ad82 (points 1 and 2, PR #245) and 5952d55 (point 3, applied by me).

Point 1 — the daemon says it

tool-surface isolation: neutralized 1 of 3 configs: .mcp.json (opencode.json absent, .autoenv absent) — the worktree copy is a stub, not the repo's file; edit the real file in the primary checkout instead

Denominator, files, and the consequence in the message itself.

Point 2 — the worker can discover it

Recorded in worktree-scoped git config, readable from inside the worktree:

git config --worktree --get-all fleet.neutralizedConfig
git config --worktree --get  fleet.neutralizedConfigNote

This is a better answer than the generated file the ticket suggested, for a reason worth keeping: worktree config lives in .git/worktrees/<nonce>/config.worktree, so it can never appear in git status — no gitignore entry, no name-collision check, no risk of a worker committing it or wasting a turn asking about it. It also reuses the exact mechanism configureEnvironmentCredentialHelper and configureHttpsUrlRewriteForSshOrigin already use in the same add() call, and unlike a flat file it has a natural multi-value shape for "which files" plus a separate key for "and why".

Point 3 — written down for the lead

Added to the CLAUDE.md project addendum. The operative sentence is the one that would have prevented the original incident: never brief a worker to edit one of these files — the edit cannot be committed, and it will not tell you so. It sits in the addendum, not the canonical block, so the byte-sync with wiki/7-Use-Cases.md is unaffected; I re-ran the documented check and it still reports in sync.

Verification

Baseline 1172 tests, 0 failures, 0 compile errors. Beyond the worker's seven mutations I ran two of my own, both killed with 0 compile errors:

Mutation Result
summary line log.info → log.debug 2 reds — the fix cannot regress to an invisible level
record into --local instead of --worktree 1 red — the config scope is pinned

That second one was the hazard I actually worried about: --local would have leaked the record into the shared repo config rather than the worktree's. It is caught.

The credential boundary is untouched — stub content and --skip-worktree marking are unchanged, and the worker's mutation G (removing the mark) still kills five existing tests.

All three points are done. Merged to main in `205ad82` (points 1 and 2, PR #245) and `5952d55` (point 3, applied by me). ### Point 1 — the daemon says it ``` tool-surface isolation: neutralized 1 of 3 configs: .mcp.json (opencode.json absent, .autoenv absent) — the worktree copy is a stub, not the repo's file; edit the real file in the primary checkout instead ``` Denominator, files, and the consequence in the message itself. ### Point 2 — the worker can discover it Recorded in **worktree-scoped git config**, readable from inside the worktree: ``` git config --worktree --get-all fleet.neutralizedConfig git config --worktree --get fleet.neutralizedConfigNote ``` This is a better answer than the generated file the ticket suggested, for a reason worth keeping: worktree config lives in `.git/worktrees/<nonce>/config.worktree`, so it can **never** appear in `git status` — no gitignore entry, no name-collision check, no risk of a worker committing it or wasting a turn asking about it. It also reuses the exact mechanism `configureEnvironmentCredentialHelper` and `configureHttpsUrlRewriteForSshOrigin` already use in the same `add()` call, and unlike a flat file it has a natural multi-value shape for "which files" plus a separate key for "and why". ### Point 3 — written down for the lead Added to the `CLAUDE.md` project addendum. The operative sentence is the one that would have prevented the original incident: **never brief a worker to edit one of these files — the edit cannot be committed, and it will not tell you so.** It sits in the addendum, not the canonical block, so the byte-sync with `wiki/7-Use-Cases.md` is unaffected; I re-ran the documented check and it still reports in sync. ### Verification Baseline **1172 tests, 0 failures, 0 compile errors**. Beyond the worker's seven mutations I ran two of my own, both killed with 0 compile errors: | Mutation | Result | |---|---| | summary line `log.info` → `log.debug` | **2 reds** — the fix cannot regress to an invisible level | | record into `--local` instead of `--worktree` | **1 red** — the config scope is pinned | That second one was the hazard I actually worried about: `--local` would have leaked the record into the shared repo config rather than the worktree's. It is caught. The credential boundary is untouched — stub content and `--skip-worktree` marking are unchanged, and the worker's mutation G (removing the mark) still kills five existing tests.
ltms closed this issue 2026-09-03 06:32:34 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#134