[RIP-SEC] workspace-write `.git` carveout does not cover nested repositories, so a sandboxed agent can plant a git hook that runs unsandboxed later
Summary
Under the workspace-write sandbox, the read-only carveout for .git/.codex/.agents — which exists
to stop a sandboxed agent from planting a git hook / config that a later unsandboxed operation would
execute — is applied only to the writable root's top-level metadata directory. A git repository
nested inside the workspace (submodule, vendored dependency, fixture repo) has its own .git
that is not the top-level one and is fully writable. A sandboxed agent can write<workspace>/vendor/sub/.git/hooks/pre-commit, which git executes the next time a git operation runs
in that subdirectory outside the sandbox.
I understand the root-level scope is intentional (PR #19846/#19847/#19852, "to avoid blocking
legitimate nested repositories"). This report is about the consequence of that choice, not the
writability itself: the deliberate non-protection of nested repos defeats the anti-hook-escape control
the top-level carveout provides.
Where
codex-rs/protocol/src/permissions.rs — metadata_child_of_writable_root inspects onlyrelative_path.components().next() (the first component below a writable root), sovendor/sub/.git/... is not recognized as protected metadata. Present on current main.
Reproduction (macOS / Seatbelt)
Real codex binary, codex sandbox -c sandbox_mode='"workspace-write"', from a workspace containing
a top-level .git and a nested vendor/sub/.git:
# control: write into the top-level .git
$ codex sandbox -c sandbox_mode='"workspace-write"' /bin/sh -c 'echo x > .git/hooks/rip-canary'
/bin/sh: .git/hooks/rip-canary: Operation not permitted # denied — carveout works
# attack: write into a nested repo's .git
$ codex sandbox -c sandbox_mode='"workspace-write"' /bin/sh -c 'echo PWNED > vendor/sub/.git/hooks/rip-canary'
$ cat vendor/sub/.git/hooks/rip-canary
PWNED # landed in the nested .git (bypass)
Impact
A sandboxed agent can plant executable git hooks in any nested repository inside the workspace; those
hooks run with the user's privileges the next time a git operation touches that nested repo outside
the sandbox. Deferred/conditional on a later out-of-sandbox git op, not immediate in-session RCE.
Caveats
- e2e was macOS/Seatbelt only. Windows issue #18918's fix ("statically exclude
.gitat any depth")
may already deny nested .git on Windows — not verified here.
- The case-insensitivity of the metadata name match is a separate policy-layer inconsistency that does
NOT yield a macOS escape (Seatbelt evaluates the deny on the kernel-canonicalized path); it is not
claimed here.
Suggested fix
If nested-repo protection is desired, match protected metadata names against every path component (or
enumerate nested .git/.codex/.agents when materializing the policy) rather than only the
writable root's first component.
---
Found with the rust-in-peace pipeline
(AI-assisted Rust vulnerability research).