Repository navigation
fix(sandbox): keep uv state the host executes out of sandbox reach - #1785
Merged
Merged
Conversation
Hooks, MCP servers, the excluded uvx / uv run commands (vetted-op-read with no prompt) and the operator's terminal run uv-managed code outside the sandbox, but the reference setup let the sandbox write both places it came from: ~/.local/share/uv (interpreters, tool venvs, credentials) and ~/.cache/uv, where uvx runs packages straight out of archive-v0. Any sandboxed command could plant code that then ran unsandboxed. ~/.local/share/uv is now readable only: dropped from the framework allowWrite, never added by sandbox-add-project-root.sh, which removes the write it once added, and rejected by sandbox-lint. The host gets its own uv cache from cache-dir in ~/.config/uv/uv.toml, which the sandbox is denied reading (uv skips an unreadable config), so sandboxed uv keeps its default writable cache. Install Step U, verify check 19, an update drift item and a troubleshooting entry cover the switch. Generated-by: Claude (Opus 5.5)
Review follow-up. Verify check 19b, install Step U and the docs only named configured allowWrite entries, so a cache-dir under /tmp or $TMPDIR would pass while the sandbox writes there without a grant. The host cache must now sit outside every allowWrite and additionalDirectories entry, every project root and the implicit temp roots, symlinks resolved, with no writable root inside it. Generated-by: Claude (Opus 5.5)
potiuk
added a commit
to potiuk/magpie
that referenced
this pull request
Oct 24, 2026
… hop checks uv's managed CPython under ~/.local/share/uv/python is now a candidate, after the system locations and newest first. The reference setup keeps ~/.local/share/uv read-only to the sandbox (apache#1785) and sandbox-lint rejects a grant over it, so that one directory is exempt from the blanket ~/.local refusal; any settings grant over it still refuses it. A link-target component that is itself a symlink is now followed hop by hop through the writable-root check, so a name inside the prefix cannot route through ~/.cache on its way to a system location. Generated-by: Claude (Opus 5.5)
potiuk
added a commit
to potiuk/magpie
that referenced
this pull request
Oct 24, 2026
… hop checks uv's managed CPython under ~/.local/share/uv/python is now a candidate, after the system locations and newest first. The reference setup keeps ~/.local/share/uv read-only to the sandbox (apache#1785) and sandbox-lint rejects a grant over it, so that one directory is exempt from the blanket ~/.local refusal; any settings grant over it still refuses it. A link-target component that is itself a symlink is now followed hop by hop through the writable-root check, so a name inside the prefix cannot route through ~/.cache on its way to a system location. Generated-by: Claude (Opus 5.5)
potiuk
added a commit
to potiuk/magpie
that referenced
this pull request
Oct 24, 2026
The ~/.local exemption for uv's managed interpreters matched the resolved form of ~/.local/share/uv/python too, so a symlink there (or at a parent) could widen it to wherever it pointed. It now holds only when that path is a real directory reached without a symlink, and matches it literally. Also regenerates the secure-setup fingerprint after the rebase onto apache#1785. Generated-by: Claude (Opus 5.5)
This was referenced Oct 24, 2026
potiuk
added a commit
that referenced
this pull request
Oct 24, 2026
…the sandbox (#1784) * fix(container-gateway-hook): never run sandbox-writable code outside the sandbox The SessionStart/SessionEnd hook runs outside the sandbox but took python3 from PATH, which is often a uv-managed interpreter under the sandbox-writable ~/.local/share/uv, and it accepted gateway sources inside the project root (the .apache-magpie/ snapshot, or an override pointing into the checkout). Any sandboxed command, which runs without a prompt, could plant code there that ran unsandboxed at the next session start. The hook now refuses any interpreter or source that resolves, through every symlink hop, under the project root, ~/.cache, ~/.local, /tmp/claude*, or a sandbox.filesystem.allowWrite / permissions.additionalDirectories entry. The interpreter comes from an operator pin or fixed Homebrew/system paths, never PATH, and runs with -I plus a one-line bootstrap instead of PYTHONPATH and -P. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): pin PATH; resolve grants per settings file The hook ran jq, git, readlink and dirname from the inherited PATH, which project settings can set; it now uses fixed system locations. Relative allowWrite / additionalDirectories entries resolve against their own settings file root (~/.claude for user scope), and a single-slash entry is counted both absolute and relative, so an ambiguous grant only refuses more. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): judge the runtime and every source file Review follow-up. The interpreter now runs by its canonical path, so no symlink on the way can be re-pointed between check and exec, and with -I -S, so no site-packages, .pth file or sitecustomize loads. Its prefix and standard library are refused when they overlap a writable root in either direction, so a grant on one stdlib file counts. The source tree is judged file by file and refused if it holds a symlink, since the whole directory goes on sys.path. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): trust nothing before it is judged Second review follow-up. The shebang names /bin/bash and helpers come only from the root-owned system directories, failing closed when settings exist but jq does not. The source override must be absolute and is resolved once, so the judged path is the path that runs. An interpreter is judged from its layout before the first exec: a virtualenv, or anything without its standard library in <prefix>/lib/python3.* (a launcher stub), is never run. Keeps to bash 3.2. Generated-by: Claude (Opus 5.5) * fix(container-gateway): stop recognises the hook launch; judge stdlib links Third review follow-up. `stop` matched only `python -m container_gateway`, so it refused every gateway the hook now launches with `-I -S -c <bootstrap>`, leaving the old policy running. It now also matches that exact bootstrap (HOOK_BOOTSTRAP, kept identical to the hook by a test), and accepts the `.../Python.app/Contents/MacOS/Python` argv a macOS framework build re-execs as, which the `-m` form never matched either. Verified live: a gateway started by the hook is stopped by it. The hook also refuses an interpreter whose prefix holds a symlink into a writable directory, checked before the first exec. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): main-checkout grants; walk linked stdlib dirs Fourth review follow-up. In a linked worktree Claude Code also applies the main checkout's settings, so their write grants (and the main checkout itself) now count as writable roots. The interpreter-prefix symlink scan follows linked directories, so a module behind a linked stdlib directory is judged too; a maze of links refuses the interpreter. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): count Edit allow rules; confirm the main checkout Fifth review follow-up. Edit/Write allow rules merge into the sandbox's write access, so their paths now count as writable roots (a bare `Edit` refuses everything). A linked worktree's .git file is itself writable, so the main checkout it names counts only when its worktrees/<name>/gitdir points back; otherwise the hook does not start the gateway. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): no escaping links in the stdlib; fold every base Sixth review follow-up, replacing the linked-directory walk with a stricter rule: <prefix>/lib/python3.X must be a real directory, and every symlink in it (site-packages aside, which -S never loads) must stay inside the prefix both lexically and once resolved, so no hop can lead out to something re-pointable. Relative grant entries count against the settings directory, the project root and the main checkout alike; an entry with `..` refuses everything. The worktree back-pointer is read from <common>/worktrees/<name>, and the git dir must be exactly that. The runtime tests now use a marker-script interpreter, so each one shows whether the hook trusted the layout far enough to execute it. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): step-wise link containment; refuse system grants Seventh review follow-up. Folding `..` before the containment test hid a writable hop in the middle of a link target; every symlink in the prefix is now followed one component at a time and must stay inside the prefix (or on its ancestors) at each step. A write grant over a system or package-manager location (/usr, /opt, /Library, ...) refuses the gateway outright, since native libraries such as Homebrew's OpenSSL live there, beyond what the prefix checks can see. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): accept Linux system Python; explicit test concat CI ran the hook with /usr/bin/python3.12, whose prefix is /usr. Scanning every symlink under /usr refused it, since Debian links legitimately leave the prefix (/etc/alternatives, sitecustomize.py into /etc), so every positive test saw an empty command. A link step may now also pass through a system location (/usr, /etc, /opt, ...): the hook already refuses to run when any write grant overlaps one, so nothing there is writable or re-pointable from the sandbox. The scan covers what -I -S loads, the standard library and the libpython beside it, rather than the whole prefix. Verified on Ubuntu 24.04 and macOS. Also makes a string concatenation in a test parameter list explicit (CodeQL: implicit concatenation in a list). Generated-by: Claude (Opus 5.5) * feat(container-gateway-hook): trust uv-managed interpreters; physical hop checks uv's managed CPython under ~/.local/share/uv/python is now a candidate, after the system locations and newest first. The reference setup keeps ~/.local/share/uv read-only to the sandbox (#1785) and sandbox-lint rejects a grant over it, so that one directory is exempt from the blanket ~/.local refusal; any settings grant over it still refuses it. A link-target component that is itself a symlink is now followed hop by hop through the writable-root check, so a name inside the prefix cannot route through ~/.cache on its way to a system location. Generated-by: Claude (Opus 5.5) * fix(container-gateway-hook): exempt only a real, unlinked uv python dir The ~/.local exemption for uv's managed interpreters matched the resolved form of ~/.local/share/uv/python too, so a symlink there (or at a parent) could widen it to wherever it pointed. It now holds only when that path is a real directory reached without a symlink, and matches it literally. Also regenerates the secure-setup fingerprint after the rebase onto #1785. Generated-by: Claude (Opus 5.5)
potiuk
added a commit
that referenced
this pull request
Oct 24, 2026
… start (#1788) * fix(setup): drop the stale ~/.local/share/uv sandbox grant at session start sandbox-add-project-root.sh removes the old ~/.local/share/uv write grant (#1785) only when it runs, so a project nobody checks out or upgrades keeps it, and sandboxed code there can still change the interpreters and tools uv runs outside the sandbox. magpie-setup's session-start hook now removes the exact entries the helper once added from the checkout's gitignored .claude/settings.local.json (and the main checkout's, in a linked worktree), and tells the user in a systemMessage. A broader grant, or one in user-scope, committed or tracked settings, is reported for the user to remove. Generated-by: Claude (Opus 5.5) * fix(setup): session-start cleanup runs no git and follows no symlink Review follow-up; the hook runs outside the sandbox over a sandbox-writable tree. It wrote through a fixed settings.local.json.tmp name a sandboxed command could plant as a symlink; it now writes a fresh mkstemp file beside the target and refuses when .claude or the file is a symlink. It ran `git check-ignore`, which the repository's own sandbox-writable config can turn into command execution (core.fsmonitor); no git runs now, and a tracked settings.local.json is cleaned too, since dropping the grant only narrows the sandbox. It followed a linked worktree's .git pointer to the main checkout, letting sandboxed code choose the directory it rewrites; it now cleans only the session's own checkout. Generated-by: Claude (Opus 5.5) * fix(setup): rewrite settings.local.json through one directory descriptor Review follow-up. Checking for symlinks and then writing by path left a window in which a sandboxed process still running from an earlier session could swap .claude for a symlink. The cleanup now opens .claude once with O_NOFOLLOW and does the read (O_NOFOLLOW), the fresh temporary file (O_EXCL|O_NOFOLLOW, random name) and the rename relative to that descriptor, so nothing that happens to the path meanwhile redirects it. Generated-by: Claude (Opus 5.5) * fix(setup): address CodeQL findings in the session-start cleanup Builds the session-start message and context as lists instead of conditional generator parts (CodeQL: potentially uninitialized local), and replaces the silent except around the temporary-file cleanup with a commented contextlib.suppress (CodeQL: empty except). Generated-by: Claude (Opus 5.5)
potiuk
added a commit
that referenced
this pull request
Oct 24, 2026
…1791) * fix(setup): session-start hook runs a trusted, isolated interpreter The SessionStart hook runs outside the sandbox but took python3 from PATH, which project settings can set and which is often a uv-managed interpreter. It now uses only fixed system and Homebrew locations, then uv's managed CPython (read-only to the sandbox since #1785), first one 3.11 or newer, and runs it with -I and a one-line bootstrap instead of PYTHONPATH and -P, so no PYTHON* variable, user site-packages or the opened project reaches sys.path. Generated-by: Claude (Opus 5.5) * fix(setup): session-start hook trusts system Python only, with -I -S Review follow-up. Trusting uv-managed interpreters needs the gateway hook's full writable-root checks, and this hook only prints a notice, so it now takes only fixed system and Homebrew locations (which no sandbox grant may cover) and otherwise prints its own notice alone. It runs with -I -S: setup_preflight needs only the standard library. Generated-by: Claude (Opus 5.5)
This was referenced Oct 24, 2026
potiuk
added a commit
that referenced
this pull request
Oct 25, 2026
Ships the fixes merged since 0.9.0.dev202610241347 to marketplace installs: the container-gateway hook hardening (#1784, #1789, #1790, #1793, #1804), host-only uv state (#1785), session-start cleanup of the stale uv grant (#1788), the trusted session-start interpreter (#1791), the sandboxed prek home (#1792) and per-machine gateway hook registration (#1797). Generated-by: Claude (Opus 5.5)
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.
Hooks, MCP servers, the excluded
uvx/uv runcommands (vetted-op-readwithout a prompt) and the operator's terminal run uv-managed code outside the sandbox. The reference setup let the sandbox write both places that code came from:~/.local/share/uv(interpreters, tool venvs, credentials) and~/.cache/uv, whereuvxruns packages straight fromarchive-v0.~/.local/share/uvbecomes read-only to the sandbox. It is dropped fromallowWrite,sandbox-add-project-root.shremoves the old grant, and sandbox-lint rejects it.cache-dirin~/.config/uv/uv.toml, which the sandbox is denied reading, so sandboxed uv keeps its default writable cache.🤖 Generated with Claude Code