A worktree link vouches only in the shape git writes it - #191
Merged
Merged
Conversation
The mutual handshake shipped in #184 checks that two files name each other. A kaibo deliberation (cast `gpt-deliberate`, GPT-5.6-sol over a GPT-5.6-luna dossier) argued that agreement is not provenance, and the argument holds: the back-link was read from whatever path the registration named, with no requirement that it be `<worktree>/.git`. Two ordinary files naming each other — `<repo>/.git/worktrees/evil/gitdir` naming `<outside>/marker`, and `marker` naming the registration back — vouched for `<outside>`. A symlinked `worktrees/<name>` entry likewise let the registration itself live outside the common dir. Both routes still require writing a file inside the directory being reached, which is the access the whole design says an attacker lacks — so this is the shape of the vouch widening past git's own layout, not a hole of the kind #184 closed. Amy's call was to hold 0.5.0 and fix it rather than ship and follow. Three checks, one test each, each test run against the code without its check: the registration canonicalizes to a direct child of `<common>/worktrees`, the registration names `<wt_root>/.git`, and that file canonically lives in the root it vouches for. A third route the deliberation raised — `<tree>/.git` symlinked at an outside repo — was probed and already refused by the tree-side handshake; that probe stays as a regression test. The two real-`git worktree add` positives were sabotage-checked as well: with the vouch emptied they fail, so they prove the mutual check still accepts the layouts git actually writes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Reviewed-by: kaibo cast `gpt-deliberate` (GPT-5.6-sol / GPT-5.6-luna)
Merged
tobert
added a commit
that referenced
this pull request
Sep 11, 2026
Version `0.4.0` → `0.5.0`, a fresh empty unreleased section, and `docs/sandbox-probes.md` stamped with the run that gates this release. **Minor, not patch**, for two reasons. #182 removed `explorer_max_turns`/`synth_max_turns` as tool arguments and CLI flags — a caller-facing break. And the read boundary changed four times: three worktree-follow escapes closed in #184, then #191 requiring git's own *shape* after a kaibo deliberation argued that mutual agreement between two files is not provenance. The changelog section was shortened before retitling, per the rule that entries get shorter over time — 70 lines to 52, no bullet dropped. The four bullets that each narrowed the same worktree rule now say what they narrow in one line apiece. ## Gates | gate | result | |---|---| | `cargo tree -i` aws-lc-rs / aws-lc-sys / mimalloc / openssl-sys | absent, negative control prints `kaish-kernel v0.17.2` | | musl build | `statically linked`, `not a dynamic executable`, runs | | suite | 1363 passed, 0 failed | | sandbox battery A–H | all clear | ## The battery Run because two triggers fired at once: `kaish-vfs` moved with the 0.17.2 bump (#187), and #191 changed containment itself. Highlights worth reading rather than trusting: every out-of-root read came back `not found` including `~/.deepseek-key`, which holds a real key this session — absence would have passed vacuously, so the file existing is what makes the result mean anything. The prefix listing was *counted*: `ls /home/atobey` returns 1 entry where the host holds 74. Battery D's `..` row canonicalizes to `/etc` and is refused by containment with the message naming the allowed set, not by a resolution failure. G3's three refusals stay byte-identical across exists / missing / unreadable, so a hostile repo gets no existence oracle. **The F3 this runbook has asked for since 2026-08-13 is now covered, and it split in two.** `write_cas` — the model-reachable deposit — refuses a source outside the allowed set, with the positive control run so the refusal is not vacuous. The CLI `kaibo cas write` does *not* refuse an outside source, and that is correct rather than a finding: the CLI caller is the operator, who can already read the file. H1's note reads as though one rule covers both surfaces; it covers the tool. **The probe caught itself again**, which is what §0 exists for. The Battery H fixture put `--state-db` inside the tree it passed as `--root`, so E1 fired, persistence went off, the CAS fell back to memory and `cas write` refused — three batteries' worth of correct behavior reading, at a glance, like a broken store. Two message drifts recorded, neither a hole: `kill 1` now answers `signalling a PID requires the subprocess capability`, and `grep` following an escaping symlink refuses with exit 1 and *no* stderr line, where every other verb in G2 names the reason — worth knowing before someone reads that silence as success. ## After merge Tag `v0.5.0` to fire the release matrix, then verify a downloaded asset the way a user would (`gh attestation verify`, `cosign verify-blob`) and run `scripts/bump-tap.sh v0.5.0`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The mutual handshake that shipped in #184 verifies that two files name each other. A kaibo deliberation over the release candidate (cast
gpt-deliberate— GPT-5.6-sol reasoning offline over a GPT-5.6-luna dossier) made the case that agreement is not provenance, and the case holds.vouched_worktreesread the back-link from whatever path the registration named. Nothing required that path to be<worktree>/.git. So two ordinary files naming each other were enough:<repo>/.git/worktrees/evil/gitdirnaming<outside>/marker, andmarkernaming the registration back, vouched for<outside>. Separately, a symlinkedworktrees/<name>entry let the registration live outside the common dir entirely, so a directory the common dir does not hold could still hand out a vouch.Both routes still require the attacker to write a file inside the directory they want kaibo to read — which is the access the design says they lack, and the reason the back-link works at all. So this is the accepted shape widening past git's own layout rather than a hole of the kind #184 closed. Amy read it that way too and chose to hold 0.5.0 for the fix rather than ship and follow: the release's headline is this boundary, and a security claim is worth stating only as narrowly as it is true.
Three checks, each pinned by exactly one test, each test run against the code with that check removed:
<common>/worktrees<wt_root>/.git, not some other file.gitcanonically lives in the root it vouches forA third route the deliberation raised —
<tree>/.gitsymlinked at an outside repo — was probed and is already refused by the tree-side handshake. The probe stays as a regression test so a later change cannot quietly drop that property.The two real-
git worktree addpositives were sabotage-checked as well: with the vouch emptied, both fail. They prove the mutual check still accepts the layouts git actually writes, rather than passing because git was missing.Suite 1356/0, clippy clean.
What this does not do: make the link files trustworthy. Text cannot authenticate its author. What it does is narrow the shape that vouches to one an attacker must write inside the directory being reached. The module doc now says that in as many words, because the old wording implied the handshake alone settled it.
🤖 Generated with Claude Code