# Row 91, the submodule escape in round 2 (Darkwing) Issue #1585. Filbert reported it in their round 2 review: a nested submodule's clean step ran a command on the host. Their session was cut off before a write-up (decision 130). I reproduced it on round 2's `packages/seat/src/checkout.mjs` (manifest `6b58085b`, request comment 28087) in Sage's slot after 12:56Z on 2026-10-13. It is real. Round 3 closes it; `build.md` here has the fix, the tests and the mutants. ## What ran A clean filter driver. Git runs a file's clean filter when it has to hash a working-tree file, and `git status` does that for a tracked file whose size is unchanged but whose stat data no longer matches the index. ## Where On the bus host, outside the session's sandbox, at the next launch of the instance. `update()` ran git status --porcelain=v1 -z --untracked-files=all --ignore-submodules=none in the clone. With `--ignore-submodules=none`, git checks every populated gitlink by starting a second `git status` inside the submodule's directory. That child git reads the submodule's own `.git/config` and `.gitattributes`. When those name a clean filter for a modified file, the child git runs it on the host. The session builds all of it inside its own clone, which it can write: a repository in a subdirectory, a filter in that repository's config, an attribute that applies it to a tracked file, a same-size edit to that file, and a gitlink to the repository staged in the clone's index. None of that touches the clone's top-level `.git/config`. A `.gitmodules` entry is not needed. The same works one level down: the gitlink's repository carries its own gitlink to an inner repository, and only the inner one names the filter, since the child git status recurses too. ## Which guard it got past Every round 2 check looks at the clone's own `.git` only: - the lstat walk covers `/.git`, not a `.git` in a working-tree subdirectory; - the config allowlist reads `/.git/config`, not a submodule's config; - the hooks check reads `/.git/hooks`. The `-c` flags in `SAFE` do reach the child git through `GIT_CONFIG_PARAMETERS`, and `gitEnv()` reaches it through the environment, so hooks, fsmonitor and the transports stay off in the submodule. Neither sets anything about filters, and the allowlist can't cover a config it never reads. Round 2 kept the clone with reason `dirty`, after the filter had run. ## What did not escape on round 2 - `git submodule add` and `git submodule update --init` in the clone write `submodule.*` keys to the top-level config and make `.git/modules`. The allowlist stopped both with `config-changed` before any git ran. - A gitlink plus `submodule.active` in the top-level config: same, `config-changed`. ## The fix in round 3 A clone with a gitlink in its index, or with anything at `.git/modules`, is kept with reason `submodule`, before `status` runs. `status` uses `--ignore-submodules=all`, and every host git call gets `-c submodule.recurse=false`. Details in `build.md`, section "Round 3". The probe I used stays in my scratch, as Sage asked. The new tests in `packages/seat/tests/checkout.test.mjs` build each case above, and reviewing round 3 doesn't need the probe.