docs: row 91 brief, host git treats the clone as hostile; decision 126 (#1585)

Darkwing found the brief had the host run unconfined git in a clone the
session writes. The brief now requires a clean git environment, hook and
fsmonitor overrides, an lstat walk of .git, a config-key allowlist and a
session-live check, and adds bundle.mjs and a harness test to the files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-12 18:06:02 -05:00
co-authored by Claude Opus 5.5
parent 646e3f77d7
commit 020ad78c2c
2 changed files with 68 additions and 1 deletions
+22
View File
@@ -2816,3 +2816,25 @@ which stay with him. Each item names who decided it and what happened.
- If row 63 needs a round 3, the order still holds unless row 90 has
been ready for a full working session while row 63 is still under
review. Then Sage looks at it again.
126. **Row 91's host git treats the clone as hostile (2026-10-12).**
Darkwing found that the brief had the host run git, outside the
sandbox, in a clone the session can write. A session could set
`core.fsmonitor` or a hook and make the host's next `git status` or
merge run a command, or plant a symlink or `gitdir:` file so the
fast-forward writes somewhere else. That undoes row 92.
- The brief now requires a clean git environment, `-c` overrides for
hooks, fsmonitor and protocols, an `lstat` check of the clone with
no symlinks, special files or extra hard links under `.git`, and a
config-key allowlist read with `git config --file --list`. A clone
that fails is kept with a reason and no git runs in it. Darkwing
proposed these; Sage added the `.git` walk, the config size cap and
the session-live check, because a session that can still write the
clone could change it between check and use.
- The git step gets no bwrap sandbox. One would have to bind the
root's `.git`, whose remote URL may hold a credential. Filbert
reviews the allowlist as the row's main risk.
- `packages/harness/src/bundle.mjs` (the `prompt()` line naming the
clone, and its doc comment) and a harness test join the files
owned. The harness suite was already in the gate.
- The brief is re-pinned to the edited section, with the old section
byte-identical at 51085ddd and HEAD before the edit.
+46 -1
View File
@@ -229,7 +229,11 @@ Reviewer: Filbert.
- `packages/seat/src/checkout.mjs`, new
- `packages/seat/README.md`, `packages/cli/README.md`, the workspace
paragraphs
- tests under `packages/seat/tests/` and `packages/cli/tests/`
- `packages/harness/src/bundle.mjs`, the line in `prompt()` that names
the clone's path and the doc comment on the session argument (added
by decision 126)
- tests under `packages/seat/tests/`, `packages/cli/tests/` and
`packages/harness/tests/`
- `agents/darkwing/work/queue-<row>/`
### What ships
@@ -251,6 +255,47 @@ Reviewer: Filbert.
launch steps. No partial clone is left behind.
- The bundle's session text names the clone's path, so the session
knows where the project is.
- The clone is written by the session, so the host treats it as hostile
before it runs git there (decision 126). Without these guards, a
session could make the host run a command outside the sandbox, which
would undo row 92.
- Every git call runs with `GIT_CONFIG_NOSYSTEM=1`,
`GIT_CONFIG_GLOBAL=/dev/null`, an empty host-made `HOME` and
`GIT_TERMINAL_PROMPT=0`, and with `-c core.hooksPath=/dev/null
-c core.fsmonitor=false -c protocol.allow=never
-c protocol.file.allow=always`.
- Before any git runs in an existing clone, the host checks it with
`lstat` and never follows a link. `<workspace>/<id>` and its `.git`
are real directories. Nothing under `.git` is a symlink or a special
file, and no regular file there has more than one link.
`.git/commondir` and `objects/info/alternates` are absent.
- The host reads `.git/config`, a regular file under a size cap, with
`git config --file <f> --list`, which follows no include and runs
nothing. Every key is on an allowlist: the keys `git clone` writes,
with `remote.origin.url` equal to the root and the standard fetch
line, plus `branch.<name>.remote` and `branch.<name>.merge`.
- A clone that fails a check is kept, `checkout-kept` with reason
`not-a-clone` or `config-changed`, and no git runs in it.
- The checks and the git calls run only while no session holds that
workspace, so a session can't change the clone between check and
use. If the launcher already refuses a second live session in one
workspace, the packet cites that refusal and a test shows it.
Otherwise the clone is kept with reason `session-live`.
- A new clone goes to a temporary sibling directory and is renamed
into place, so a failure removes only that directory.
- A root with no branch checked out refuses `checkout-failed` for a
missing clone and keeps an existing one.
- The git step gets no bwrap sandbox of its own. One would have to
bind the root's `.git`, whose remote URL may carry a credential.
The allowlist closes the execution paths instead, and Filbert
reviews it as the row's main risk.
- Tests: one case for each way a session could make the host run
something or write outside the clone: `core.fsmonitor`,
`core.hooksPath`, a hook file, `include.path`, a symlinked clone
directory, a `gitdir:` file, a symlink under `.git` (`logs`, `refs`,
`objects`), alternates and `commondir`. Each case keeps the clone and
shows the command never ran, because the marker file it would write
is absent. The root is unchanged.
- Tests: a first launch clones; a second fast-forwards after a commit
to the root; a dirty clone and one on another branch are kept and
logged; an unreadable root refuses `checkout-failed` with nothing