From da6ee1da6e55bcd6922db27c6515be0af05b34bc Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sun, 25 Oct 2026 02:13:42 +0200 Subject: [PATCH 1/2] fix(container-gateway-hook): derive the checkout root without repository config A .git directory planted in a sandbox-writable checkout could set core.worktree to a parent directory: git then reports a toplevel that still contains the session cwd (so the existing containment check passes) but keys the worktree records differently, so the main checkout's record is never consulted and its grants are skipped. The hook now also finds the root the way the filesystem shows it, the nearest directory holding a .git, refuses when git's toplevel differs, and refuses whenever core.worktree is set. The new test fails against the previous hook. Generated-by: Claude (Opus 5.5) --- .../setup_preflight/isolated_fingerprint.py | 2 +- .../agent-isolation/container-gateway-hook.sh | 26 +++++++++++++++++++ .../tests/test_container_gateway_hook.py | 24 +++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py b/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py index 865128204..07acea5f9 100644 --- a/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py +++ b/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py @@ -21,4 +21,4 @@ for installs that do not carry the framework source (see `isolated.py`). """ -FRAMEWORK_FINGERPRINT = "sha256:198dc3e80e1af151" +FRAMEWORK_FINGERPRINT = "sha256:c767343164546c44" diff --git a/tools/agent-isolation/container-gateway-hook.sh b/tools/agent-isolation/container-gateway-hook.sh index 32a289553..8d04b8e18 100755 --- a/tools/agent-isolation/container-gateway-hook.sh +++ b/tools/agent-isolation/container-gateway-hook.sh @@ -132,6 +132,32 @@ if [[ $cwd != "$root" && $cwd != "$root"/* ]]; then log "git reports $root as the toplevel of $cwd, which it does not contain; not starting the container gateway" exit 0 fi +# The root as the filesystem shows it, read without any repository config: the +# nearest directory above the cwd holding a .git. git's toplevel must be that +# directory, and core.worktree must be unset: a .git directory planted in a +# sandbox-writable checkout could otherwise name a parent as the toplevel, which +# still contains the cwd but keys the worktree records differently, so an +# existing record would never be consulted. +fs_root="" +probe_dir="$cwd" +while :; do + if [[ -e $probe_dir/.git || -L $probe_dir/.git ]]; then + fs_root="$probe_dir" + break + fi + [[ $probe_dir == / ]] && break + probe_dir="$(dirname -- "$probe_dir")" +done +if [[ -n $fs_root ]]; then + if [[ $fs_root != "$root" ]]; then + log "git reports $root as the toplevel of $cwd, but the checkout is $fs_root; not starting the container gateway" + exit 0 + fi + if [[ -n "$(git -C "$root" config --get core.worktree 2>/dev/null)" ]]; then + log "core.worktree is set for $root; not starting the container gateway" + exit 0 + fi +fi # The home directory from the account database (`~user` reads it, not $HOME), # so a $HOME redirected by the environment cannot hide the real ~/.cache, diff --git a/tools/agent-isolation/tests/test_container_gateway_hook.py b/tools/agent-isolation/tests/test_container_gateway_hook.py index a219048e8..54a855e93 100644 --- a/tools/agent-isolation/tests/test_container_gateway_hook.py +++ b/tools/agent-isolation/tests/test_container_gateway_hook.py @@ -782,3 +782,27 @@ def test_a_writable_record_directory_refuses_even_a_plain_checkout(tmp_path: Pat lay.settings({"sandbox": {"filesystem": {"allowWrite": [str(lay.home / ".claude" / "magpie-gateway")]}}}) done = run("start", lay.project, home=lay.home) assert done.stdout == "" and "is sandbox-writable" in done.stderr + + + +def test_a_planted_core_worktree_cannot_move_the_record_key(tmp_path: Path) -> None: + """A fake .git naming a parent as the toplevel still contains the cwd, but would + key the worktree records differently and skip the existing one.""" + lay = Layout(tmp_path) + worktree = _linked_worktree(lay) + run("start", worktree, home=lay.home) + # A plain .git directory (so the checkout no longer looks linked) whose config + # names the parent as the work tree: git's toplevel then contains the cwd. + (worktree / ".git").unlink() + subprocess.run(["git", "init", "-q", str(worktree)], check=True) + subprocess.run(["git", "-C", str(worktree), "config", "core.worktree", str(lay.base)], check=True) + done = run("stop", worktree, home=lay.home) + assert done.stdout == "" + assert "but the checkout is" in done.stderr + + +def test_core_worktree_on_a_plain_checkout_refuses(tmp_path: Path) -> None: + lay = Layout(tmp_path) + subprocess.run(["git", "-C", str(lay.project), "config", "core.worktree", str(lay.project)], check=True) + done = run("start", lay.project, home=lay.home) + assert done.stdout == "" and "core.worktree is set" in done.stderr From c33d7fde4c688cd3f3bbefaaccc8f1a15a1133bf Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sun, 25 Oct 2026 02:18:24 +0200 Subject: [PATCH 2/2] fix(container-gateway-hook): refuse core.worktree everywhere; never discover a bare repo Review follow-up, defense in depth. The root checks ran only when a .git was found on the way up. Now core.worktree refuses whether or not one is, a repository git finds with no .git on the way up refuses, and every git call runs with safe.bareRepository=explicit, so HEAD / config / objects planted in a project without .git are never treated as a repository. The new test is a regression guard: git already declined a toplevel from inside such a directory, so it did not reproduce as a bypass on the previous hook. Generated-by: Claude (Opus 5.5) --- .../setup_preflight/isolated_fingerprint.py | 2 +- .../agent-isolation/container-gateway-hook.sh | 26 ++++++++++++------- .../tests/test_container_gateway_hook.py | 15 +++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py b/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py index 07acea5f9..f00c82004 100644 --- a/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py +++ b/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py @@ -21,4 +21,4 @@ for installs that do not carry the framework source (see `isolated.py`). """ -FRAMEWORK_FINGERPRINT = "sha256:c767343164546c44" +FRAMEWORK_FINGERPRINT = "sha256:802fecf15e0df3ce" diff --git a/tools/agent-isolation/container-gateway-hook.sh b/tools/agent-isolation/container-gateway-hook.sh index 8d04b8e18..829698da8 100755 --- a/tools/agent-isolation/container-gateway-hook.sh +++ b/tools/agent-isolation/container-gateway-hook.sh @@ -120,6 +120,10 @@ done log() { printf '%s: %s\n' "${0##*/}" "$*" >&2; } +# Every git call: never discover a bare repository, which sandboxed code could +# plant as HEAD / config / objects in a project that has no .git of its own. +git() { command git -c safe.bareRepository=explicit "$@"; } + payload="$(cat 2>/dev/null || true)" cwd="$(printf '%s' "$payload" | jq -r '.cwd // empty' 2>/dev/null || true)" [[ -n $cwd ]] || cwd="$PWD" @@ -148,15 +152,19 @@ while :; do [[ $probe_dir == / ]] && break probe_dir="$(dirname -- "$probe_dir")" done -if [[ -n $fs_root ]]; then - if [[ $fs_root != "$root" ]]; then - log "git reports $root as the toplevel of $cwd, but the checkout is $fs_root; not starting the container gateway" - exit 0 - fi - if [[ -n "$(git -C "$root" config --get core.worktree 2>/dev/null)" ]]; then - log "core.worktree is set for $root; not starting the container gateway" - exit 0 - fi +git_toplevel="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || true)" +if [[ -z $fs_root && -n $git_toplevel ]]; then + log "git finds a repository for $cwd but there is no .git on the way up; not starting the container gateway" + exit 0 +fi +if [[ -n $fs_root && $fs_root != "$root" ]]; then + log "git reports $root as the toplevel of $cwd, but the checkout is $fs_root; not starting the container gateway" + exit 0 +fi +# Checked whether or not a .git was found: core.worktree is what moves a toplevel. +if [[ -n "$(git -C "$cwd" config --get core.worktree 2>/dev/null)" ]]; then + log "core.worktree is set for $cwd; not starting the container gateway" + exit 0 fi # The home directory from the account database (`~user` reads it, not $HOME), diff --git a/tools/agent-isolation/tests/test_container_gateway_hook.py b/tools/agent-isolation/tests/test_container_gateway_hook.py index 54a855e93..99fd232c0 100644 --- a/tools/agent-isolation/tests/test_container_gateway_hook.py +++ b/tools/agent-isolation/tests/test_container_gateway_hook.py @@ -806,3 +806,18 @@ def test_core_worktree_on_a_plain_checkout_refuses(tmp_path: Path) -> None: subprocess.run(["git", "-C", str(lay.project), "config", "core.worktree", str(lay.project)], check=True) done = run("start", lay.project, home=lay.home) assert done.stdout == "" and "core.worktree is set" in done.stderr + + +def test_a_planted_bare_repository_is_not_discovered(tmp_path: Path) -> None: + """In a project with no .git, HEAD / config / objects planted in the cwd would make + git treat it as a bare repository whose core.worktree names a parent.""" + lay = Layout(tmp_path) + plain = lay.base / "plain" + plain.mkdir() + subprocess.run(["git", "init", "-q", "--bare", str(plain)], check=True) + subprocess.run(["git", "--git-dir", str(plain), "config", "core.worktree", str(lay.base)], check=True) + subprocess.run(["git", "--git-dir", str(plain), "config", "core.bare", "false"], check=True) + done = run("start", plain, home=lay.home) + # Either refused, or served for the directory itself: never for the parent. + assert done.stdout == "" or f"--project={plain.resolve()} " in done.stdout + assert f"--project={lay.base} " not in done.stdout