Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
"~/.local/bin/"
],
"allowWrite": [
"~/.cache/",
"~/.local/share/uv/"
"~/.cache/"
]
},
"network": {
Expand Down Expand Up @@ -91,6 +90,7 @@
"Read(~/.config/apache-magpie/**)",
"Edit(~/.config/apache-magpie/local-tracker-parsers.json)",
"Read(~/.config/gcloud/**)",
"Read(~/.config/uv/uv.toml)",
"Read(~/.azure/**)",
"Read(//**/.env)",
"Read(//**/.env.local)",
Expand All @@ -102,6 +102,7 @@
"Edit(.apache-magpie-overrides/tools/vetted-ops/**)",
"Edit(~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/**)",
"Edit(~/.claude/magpie/**)",
"Edit(~/.config/uv/**)",
"Edit(~/.claude/plugins/cache/apache-magpie/magpie-adversarial-review/**)",
"Edit(~/.claude/scripts/**)",
"Bash(curl *)",
Expand Down
48 changes: 42 additions & 6 deletions docs/setup/sandbox-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,19 @@
- [Root cause](#root-cause-8)
- [Fix](#fix-8)
- [Notes](#notes-8)
- [Git hooks silently skipped for commits made inside the sandbox](#git-hooks-silently-skipped-for-commits-made-inside-the-sandbox)
- [`uv python install` or `uv tool install` fails inside the sandbox](#uv-python-install-or-uv-tool-install-fails-inside-the-sandbox)
- [Symptom](#symptom-9)
- [Root cause](#root-cause-9)
- [Fix](#fix-9)
- [Notes](#notes-9)
- [Reads of `~/.claude/magpie` or `/tmp/claude-<uid>` ask for approval every time](#reads-of-claudemagpie-or-tmpclaude-uid-ask-for-approval-every-time)
- [Git hooks silently skipped for commits made inside the sandbox](#git-hooks-silently-skipped-for-commits-made-inside-the-sandbox)
- [Symptom](#symptom-10)
- [Root cause](#root-cause-10)
- [Fix](#fix-10)
- [Notes](#notes-9)
- [Reads of `~/.claude/magpie` or `/tmp/claude-<uid>` ask for approval every time](#reads-of-claudemagpie-or-tmpclaude-uid-ask-for-approval-every-time)
- [Symptom](#symptom-11)
- [Root cause](#root-cause-11)
- [Fix](#fix-11)
- [Notes](#notes-10)
- [Adding a new entry](#adding-a-new-entry)

Expand Down Expand Up @@ -1030,7 +1034,7 @@ failure on `~/.gitconfig`.
Claude Code filesystem allowlist. The framework's committed
`.claude/settings.json` lists `~/.local/bin/`, `~/.local/share/uv/`,
`~/.cache/`, `~/.gitconfig` and `~/.config/git/` under
`sandbox.filesystem.allowRead` (and the first three writable under
`sandbox.filesystem.allowRead` (and `~/.cache/` writable under
`allowWrite`), carving them out of `denyRead: ["~/"]`. The harness
does not apply those project-scope entries: the effective sandbox
denies every one of them, while the same kind of entry in
Expand Down Expand Up @@ -1066,8 +1070,8 @@ project root it already adds:
],
"allowWrite": [
"/home/<you>/code/<repo>",
"/home/<you>/.cache",
"/home/<you>/.local/share/uv"
"/home/<you>/.cache"
// never ~/.local/share/uv: see the next entry
]
}
}
Expand Down Expand Up @@ -1096,6 +1100,38 @@ Confirm with `prek --version`, or with the doctor skill's

---

## `uv python install` or `uv tool install` fails inside the sandbox

### Symptom

A sandboxed `uv python install`, `uv tool install`, or a `uv run` / `prek
run` that needs an interpreter not installed yet, fails writing under
`~/.local/share/uv/` with `Operation not permitted` (macOS) or
`Read-only file system` (Linux). Commands that use an interpreter or tool
already installed keep working.

### Root cause

Deliberate. `~/.local/share/uv/` holds the interpreters and tool venvs that
hooks, MCP servers, the excluded `uvx` / `uv run` commands and your own
terminal run outside the sandbox, so the sandbox may read it but not write
it ([Host-only uv state](secure-agent-setup.md#host-only-uv-state)). Older
versions of `sandbox-add-project-root.sh` granted the write; the current one
removes it.

### Fix

Run the install from a terminal, outside the sandbox:

```bash
uv python install 3.13
uv tool install prek
```

Do not add `~/.local/share/uv` back to `allowWrite`: `sandbox-lint` rejects it.

---

## Git hooks silently skipped for commits made inside the sandbox

### Symptom
Expand Down
81 changes: 74 additions & 7 deletions docs/setup/secure-agent-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [Wiring the check script into a weekly routine](#wiring-the-check-script-into-a-weekly-routine)
- [The framework's own `.claude/settings.json`](#the-frameworks-own-claudesettingsjson)
- [Read-only vetted ops never prompt](#read-only-vetted-ops-never-prompt)
- [Host-only uv state](#host-only-uv-state)
- [Project-root coverage in the sandbox allowlists](#project-root-coverage-in-the-sandbox-allowlists)
- [Why project-local, not user-scope and not committed-project](#why-project-local-not-user-scope-and-not-committed-project)
- [Security rationale — why project-local is safe to write to](#security-rationale--why-project-local-is-safe-to-write-to)
Expand Down Expand Up @@ -555,8 +556,8 @@ The full block below is the two merged, annotated.
"~/.ssh/id_ed25519_sk.pub" // ONLY with `gpg.format=ssh`: the public half git hands to `ssh-keygen -Y sign`. Use the file `git config user.signingkey` names (see sandbox-troubleshooting.md)
],
"allowWrite": [
"~/.cache/", // uv lock files, prek log + state, ruff/mypy caches, prek's rustup toolchains + cargo registry
"~/.local/share/uv/" // uv's tool venvs (prek installs new hook envs here)
"~/.cache/" // uv's sandbox-only cache, prek log + state, ruff/mypy caches, prek's rustup toolchains + cargo registry
// never ~/.local/share/uv/: readable only — its interpreters and tool venvs run outside the sandbox (see "Host-only uv state")
]
},
"network": {
Expand Down Expand Up @@ -708,6 +709,8 @@ The full block below is the two merged, annotated.
"Read(~/.config/apache-magpie/**)", // Gmail OAuth token: only the oauth-draft MCP server reads it, outside the sandbox
"Edit(~/.config/apache-magpie/local-tracker-parsers.json)", // the user's custom-parser confirmations: only `trust-parser`, run by the user, writes them
"Read(~/.config/gcloud/**)", "Read(~/.azure/**)",
"Read(~/.config/uv/uv.toml)", // the host-only uv cache-dir: unreadable inside the sandbox, so sandboxed uv keeps its own cache (see "Host-only uv state")
"Edit(~/.config/uv/**)",
"Read(//**/.env)", "Read(//**/.env.local)", "Read(//**/.env.*.local)",
"Bash(curl *)", "Bash(wget *)", // network egress via Bash bypasses the sandbox proxy
"Bash(aws *)", "Bash(gcloud *)", "Bash(az *)", "Bash(kubectl *)",
Expand Down Expand Up @@ -1000,6 +1003,46 @@ An agent definition that narrows its tools, such as a read-only gatherer's `tool
The imperative covers the read dispatcher only.
`vetted-op-tracker` and `vetted-op` write: they keep their `ask` rule and never go in `allow` ([why](#the-frameworks-own-claudesettingsjson)), so a skill runs them from the foreground session, never from a background agent.

## Host-only uv state

Several things run Python through uv **outside** the sandbox: the excluded `uvx --from ~/.claude/magpie/… ` and `uv run --project ~/.claude/magpie/…` commands (`vetted-op-read` among them, with no prompt at all), MCP servers launched with `uv`, hooks that call `python3`, and every `uv` / `prek` you run from a terminal.
Whatever they execute must live where a sandboxed command cannot write, or any sandboxed command — which runs without a prompt — can plant code that runs unsandboxed the next time one of them starts.
Two places used to break that rule:

- **`~/.local/share/uv/`** holds uv's managed interpreters (often what `python3` on `PATH` resolves to), its tool venvs (`prek`, `ruff`) and its credentials store.
It stays in `allowRead`, so sandboxed uv can still use the interpreters already installed, but it is never in `allowWrite`.
The cost: a sandboxed `uv python install` or `uv tool install` fails; run those from a terminal.
- **`~/.cache/uv/`** is the cache `uvx` runs from: `uvx --from <path> <cmd>` executes the unpacked package straight out of `archive-v0/`.
Sandboxed uv needs a writable cache, and Claude Code has no environment that applies only inside the sandbox, so the split goes through a file the sandbox cannot read.

The user-level uv config gives the host its own cache, and `Read(~/.config/uv/uv.toml)` hides it from the sandbox:

```toml
# ~/.config/uv/uv.toml — written by setup-isolated-setup-install (Step U).
# Host-only uv state. The sandbox is denied reading this file, so sandboxed uv
# keeps its default, sandbox-writable cache (~/.cache/uv); everything uv runs
# outside the sandbox uses a cache no sandboxed command can write.
# Absolute path: uv does not expand `~` here, and a relative path would land in
# the working directory.
cache-dir = "/home/<you>/.local/share/uv-host/cache"
```

uv silently skips a user config it cannot read, so the sandbox side needs nothing else.
Keep the host cache outside every directory a sandboxed command can write, in every project: each `allowWrite` and `additionalDirectories` entry, each project root, and the roots the sandbox writes without a grant (`/tmp`, `/private/tmp`, `/var/folders`, `$TMPDIR`). `~/.cache/` is granted to all projects, which is why it does not live there.
`Edit(~/.config/uv/**)` keeps the agent's own file tools off the config.

**After switching, reinstall what the host executes** from a terminal, since anything the sandbox could write before may already have been changed:

```bash
d="$(uv python dir)"
uv python install --reinstall $(uv python list --only-installed --output-format json \
| jq -r --arg d "$d" '.[] | select(.path != null and (.path | startswith($d))) | .version' | sort -u)
uv tool upgrade --all --reinstall
```

The old `~/.cache/uv/` stays as the sandbox's cache; the host never reads it again.
`sandbox-lint` fails a settings file that makes `~/.local/share/uv` writable or lacks the two `~/.config/uv` deny rules; `setup-isolated-setup-verify` checks the config file itself.

## Project-root coverage in the sandbox allowlists

The `.` entry in `sandbox.filesystem.allowRead` is **intended** to
Expand Down Expand Up @@ -3291,7 +3334,22 @@ Then walk through:
them to the project-local `settings.local.json` instead. See
[Working directories under the read-outside-working-directories block](#working-directories-under-the-read-outside-working-directories-block).

6. **(Optional) Waiting-for-input terminal tint.** Ask me whether
6. **Host-only uv state.** Read `~/.config/uv/uv.toml` if it
exists. Unless it already sets a `cache-dir` outside every
directory a sandboxed command can write (`allowWrite` and
working directories, project roots, `/tmp`, `/var/folders`,
`$TMPDIR`), stage the file with
`cache-dir = "<my resolved $HOME>/.local/share/uv-host/cache"` added
(an absolute path: uv does not expand `~` there) and give me the
command to put it in place — `Edit(~/.config/uv/**)` keeps you from
writing it, and an existing file's other settings must survive.
Confirm no settings scope lists `~/.local/share/uv` (or a parent) in
`allowWrite` and that `Read(~/.config/uv/uv.toml)` and
`Edit(~/.config/uv/**)` are in `permissions.deny`. Then hand me the
reinstall commands from
[Host-only uv state](#host-only-uv-state) to run in a terminal.

7. **(Optional) Waiting-for-input terminal tint.** Ask me whether
I want the terminal background to tint while Claude is waiting
on me (a pure quality-of-life signal, no security effect).
**Default no.** Only if I say yes: copy
Expand All @@ -3309,7 +3367,7 @@ Then walk through:
default. See
[Waiting-for-input terminal tint](#waiting-for-input-terminal-tint).

7. **(Optional) Hardware security key.** Ask whether I sign
8. **(Optional) Hardware security key.** Ask whether I sign
commits or authenticate to GitHub with a hardware key (YubiKey,
Nitrokey, any OpenPGP card). **Default no.** Only if I say yes:
hand me `ykman openpgp info` to run myself and read back the
Expand Down Expand Up @@ -3341,7 +3399,7 @@ Then walk through:
sandboxed signed commit fails with `cannot exec`. See
[Hardware security keys](#hardware-security-keys--signing-and-authentication).

8. **(Optional) roborev.** Only if `command -v roborev` finds it
9. **(Optional) roborev.** Only if `command -v roborev` finds it
(a third-party companion; skip otherwise). Add
`"roborev *"` to `sandbox.excludedCommands`, `Bash(roborev *)` to
`permissions.ask` with **no** `allow` for any roborev command, and
Expand All @@ -3351,12 +3409,12 @@ Then walk through:
macOS (Linux ignores `allowUnixSockets`), offer the setup skill's Step B.2 (a fixed `unix://` daemon socket and
`~/.roborev/` readable — never writable — from the sandbox), and tell
me any sandboxed command can then drive roborev without the prompt.
9. **Verify.** After everything is in place, walk through the
10. **Verify.** After everything is in place, walk through the
Verification checks from the next section of this document
("Verification — Via a Claude Code prompt") and report
✓ done / ✗ missing / ⚠ partial for each piece.

10. **Offer shared-config sync.** Once the install lands, propose
11. **Offer shared-config sync.** Once the install lands, propose
running `setup-shared-config-sync` to commit + push the
user-scope config this install just wired up to my private
`~/.claude-config` dotfile repo, so my other machines pick it
Expand Down Expand Up @@ -3601,6 +3659,15 @@ below and report ✓ done / ✗ missing / ⚠ partial, with the evidence
roborev `post-commit` in a shared whole-user hooks
directory is ✗; an unrecognised `.roborev.toml` under the hook, and a
roborev agent hook or MCP server in the harness, are ⚠.
18. **Host-only uv state.** `~/.config/uv/uv.toml` sets `cache-dir` to an
absolute path outside every directory a sandboxed command can write —
`allowWrite` and `additionalDirectories` entries, project roots, `/tmp`,
`/var/folders` and `$TMPDIR` (✗ when missing, relative, or overlapping one); no scope makes
`~/.local/share/uv` or a parent writable (✗); and `permissions.deny`
contains `Read(~/.config/uv/uv.toml)` and `Edit(~/.config/uv/**)` (✗
when missing). Probe it: `uv cache dir` inside the sandbox prints the
default cache, not the configured one (✗ if it prints the host cache —
the sandbox can read the config).
```

Re-run either form after every Claude Code upgrade — the sandbox
Expand Down
32 changes: 30 additions & 2 deletions plugins/magpie-setup/skills/isolated-setup-install/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ when_to_use: >-
If it is already installed, use `setup-isolated-setup-verify` to check
it or `setup-isolated-setup-update` to refresh it.
capability: capability:platform
surface_hash: sha256:fe5f2a0f66586cb2
surface_hash: sha256:6358702510d21c26
license: Apache-2.0
measured_tokens: 7337
measured_tokens: 7793
---

<!-- Placeholder convention (see AGENTS.md#placeholder-convention-used-in-skill-files):
Expand Down Expand Up @@ -463,6 +463,34 @@ check three things:
Rationale and the rules in full:
[`docs/setup/secure-agent-setup.md` → Working directories under the read-outside-working-directories block](../../../../docs/setup/secure-agent-setup.md#working-directories-under-the-read-outside-working-directories-block).

### Step U — Host-only uv state

The excluded `uvx` / `uv run` commands, MCP servers launched with `uv`, hooks
that call `python3` and the operator's own terminal all run uv-managed code
outside the sandbox. None of it may come from a directory a sandboxed command
can write:

- **`~/.local/share/uv` is read-only to the sandbox.** No scope may list it,
or a parent of it, in `allowWrite` or `additionalDirectories`. The Step P
helper removes the write an earlier version added; propose removing any other
by hand.
- **The host gets its own uv cache.** Read `~/.config/uv/uv.toml`. Unless it
sets a `cache-dir` that is absolute and, symlinks resolved, outside every
directory a sandboxed command can write (every `allowWrite` and `additionalDirectories` entry in any scope, every project root, and the roots the sandbox writes without a grant (`/tmp`, `/private/tmp`, `/var/folders`, `/private/var/folders`, `$TMPDIR`)),
stage it with `cache-dir = "<resolved $HOME>/.local/share/uv-host/cache"`
added, keeping every other setting, and give the operator the copy command:
`Edit(~/.config/uv/**)` keeps the agent from writing it. Never `~` (uv does
not expand it there) and never a relative path.
- **The sandbox cannot read that file.** `permissions.deny` carries
`Read(~/.config/uv/uv.toml)` and `Edit(~/.config/uv/**)` (both in the
framework's `.claude/settings.json`, so Step 2's copy brings them). Sandboxed
uv then keeps its default, writable cache.

Then hand the operator the reinstall commands from the doc to run in a
terminal: interpreters and tools the sandbox could write before may already
have been changed. Rationale and commands:
[`docs/setup/secure-agent-setup.md` → Host-only uv state](../../../../docs/setup/secure-agent-setup.md#host-only-uv-state).

### Step T — Local agent settings

The committed `.claude/settings.json` is only the shared floor; the rest
Expand Down
5 changes: 4 additions & 1 deletion plugins/magpie-setup/skills/isolated-setup-update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ when_to_use: >-
capability: capability:platform
surface_hash: sha256:1f327e069312dad2
license: Apache-2.0
measured_tokens: 6254
measured_tokens: 6446
---

<!-- Placeholder convention (see AGENTS.md#placeholder-convention-used-in-skill-files):
Expand Down Expand Up @@ -168,6 +168,9 @@ Walk each:
Together they let any sandboxed command, which runs without a prompt, use the user's Gmail OAuth token.
Report their *presence* in any scope as a **must-fix**, not dead weight, and propose removing all three; keep the `Read(~/.config/apache-magpie/**)` deny.
Drafting keeps working through the `oauth-draft` MCP server, which runs outside the sandbox; the `oauth-draft-*` CLIs need an approved run or a per-machine absolute-path exclusion ([`docs/setup/secure-agent-setup.md` → The framework's own `.claude/settings.json`](../../../../docs/setup/secure-agent-setup.md#the-frameworks-own-claudesettingsjson)).
- **`~/.local/share/uv` writable, or no host-only uv cache.**
Earlier defaults put `~/.local/share/uv/` in `allowWrite`, where the interpreters and tool venvs that hooks, MCP servers and the excluded `uvx` / `uv run` commands run outside the sandbox live; and `uvx` ran from the shared, sandbox-writable `~/.cache/uv`.
Report a write grant covering `~/.local/share/uv`, a `~/.config/uv/uv.toml` without an absolute host `cache-dir`, or a missing `Read(~/.config/uv/uv.toml)` / `Edit(~/.config/uv/**)` deny as a **must-fix**; the remedy is `setup-isolated-setup-install` Step U ([Host-only uv state](../../../../docs/setup/secure-agent-setup.md#host-only-uv-state)).
- **`enableWeakerNetworkIsolation: true`.**
It is not there for lychee, which runs offline.
It lets native-TLS CLI tools verify TLS through the sandbox's TLS-terminating proxy — the mechanism the schema notes for `gh` / `gcloud` / `terraform`.
Expand Down
Loading
Loading