Skip to content

fix(setup): session-start hook runs a fixed bash and only a root-owned Python - #1794

Merged
potiuk merged 2 commits into
apache:mainfrom
potiuk:fix/setup-hook-trusted-launch
Oct 24, 2026
Merged

potiuk merged 2 commits into
apache:mainfrom
potiuk:fix/setup-hook-trusted-launch

Conversation

@potiuk

@potiuk potiuk commented Oct 24, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to #1791. The magpie-setup SessionStart hook (check-upgrade.sh) runs outside the sandbox, and its session environment comes from settings a project can set:

  • bash came from PATH. The plugin wired bash "${CLAUDE_PLUGIN_ROOT}/hooks/check-upgrade.sh" and the shebang was #!/usr/bin/env bash, so a project-chosen bash could run unsandboxed — as could BASH_ENV and exported functions. The hook is now wired as /bin/bash -p <path>, the shebang is #!/bin/bash -p (privileged mode ignores BASH_ENV, exported functions and SHELLOPTS), and PATH is pinned.
  • The interpreter was only located, not vetted. It ran the first python3 in fixed locations, which includes macOS's /usr/bin/python3 launcher stub (it execs whatever DEVELOPER_DIR names) and Homebrew (owned by the sandbox user, so a sandboxed command can replace it), with the whole environment. It now runs a Python only when the sandbox cannot rewrite it by file permissions: every path component and symlink hop, the standard library of its own version (lib/ or lib64/), and the libraries beside it are root-owned and writable by neither group nor others; not a virtualenv, not a macOS framework build. It runs by its real path under env -i.
  • The ~/.local/share/uv cleanup no longer goes missing silently. That cleanup (fix(sandbox): keep uv state the host executes out of sandbox reach #1785) lives in the pre-flight. Where no Python qualifies — every Homebrew-only Mac — the hook now reads the sandbox write grants itself (read-only, jq) and reports any that still cover ~/.local/share/uv, naming the files, or says it could not check.

Trade-off, by decision: on a machine with no root-owned Python 3.11+ (Homebrew-only macOS), the pre-flight does not run, so the isolated-setup warning from #1791 is not shown there; the uv grant is reported rather than removed. A settings-grant analysis that would keep Homebrew was tried and dropped: it kept turning up bypasses, while file ownership is a property no settings grant can change.

Type of change

  • Cross-cutting (sandbox / agent isolation)

Test plan

  • prek run on the changed files passes
  • pytest tools/setup-preflight/tests/test_session_hook.py: 15 passed
  • New tests: the wired command and shebang pin /bin/bash -p; a bash on PATH never runs; BASH_ENV / an exported function never run; whatever interpreter the hook picks (read from its own xtrace) is root-owned all the way, never Homebrew; the stale-grant fallback reports covering and nested write grants (~, ~/.local, ~/.local/share/uv/python, ~/.local/./share//uv), ignores an allowRead, and says when it could not read a settings file; a relative CLAUDE_PROJECT_DIR outside git does not hang. The bash-pinning and interpreter tests fail on the hook before this change.
  • The ownership rule's rejection paths (a non-root-owned file or directory link inside a standard library) need root-owned fixtures and are covered by review, not by a test.
  • Several rounds of roborev review; the last clean

RFC-AI-0004 compliance

  • Sandbox — closes ways for a project's settings or a sandboxed command to get code run outside the sandbox at session start

Gen-AI disclosure: drafted with Claude Code (Opus 5); reviewed by the submitter.

🤖 Generated with Claude Code

…d Python

The magpie-setup SessionStart hook runs outside the sandbox. Its bash
came from PATH (the plugin wired `bash <path>`, the shebang was
`#!/usr/bin/env bash`), which project settings can set, and so did
BASH_ENV and exported functions. It now runs as /bin/bash -p with a
fixed PATH.

It ran the first python3 in fixed locations, including macOS's
/usr/bin/python3 launcher stub and Homebrew, which the sandbox user
owns. It now runs an interpreter only when every path component and
symlink hop, its exact-version standard library and the libraries
beside it are root-owned and not group/world-writable, never a
framework build, by its real path under env -i.

Without a trusted Python the pre-flight's ~/.local/share/uv grant
cleanup cannot run; the hook then reports a remaining grant (or that it
could not check) instead of staying silent.

Generated-by: Claude Opus 5
@github-actions github-actions Bot added family:setup setup-* skills family:tools tools/* substrate:setup Tool substrate: adopter-side setup state resolved at runtime (lock parsing, floors, fingerprints) labels Oct 24, 2026
@potiuk
potiuk enabled auto-merge (squash) October 24, 2026 23:25
Comment thread tools/setup-preflight/tests/test_session_hook.py Fixed
CodeQL: `message` looked possibly unbound after `pytest.fail` in an
except branch. Assert the JSON shape first, then parse.

Generated-by: Claude Opus 5
@potiuk
potiuk merged commit 95d27bd into apache:main Oct 24, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

family:setup setup-* skills family:tools tools/* substrate:setup Tool substrate: adopter-side setup state resolved at runtime (lock parsing, floors, fingerprints)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants