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
4 changes: 2 additions & 2 deletions docs/mode-economics.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ special-token spellings counted as ordinary text.
Coverage: **75 of 75 local `skills/*/SKILL.md` files**.
External `source.md` redirects and harness symlinks are excluded.

Measurement manifest SHA-256: `81b7316102c485194ddcd519a69ddf6d84e150ada81cced81da97868419e0253`.
Measurement manifest SHA-256: `68fd8428f555f7d70146f0a422986df178cde82c3c148be66e36878fbcf616dc`.

| Skill file | Measured tokens | Source SHA-256 (first 16 characters) |
|---|---:|---|
Expand Down Expand Up @@ -192,7 +192,7 @@ Measurement manifest SHA-256: `81b7316102c485194ddcd519a69ddf6d84e150ada81cced81
| [setup-isolated-setup-doctor](../skills/setup-isolated-setup-doctor/SKILL.md) | 4,720 | `f24d416c966167f7` |
| [setup-isolated-setup-install](../skills/setup-isolated-setup-install/SKILL.md) | 3,879 | `9c7d49c9e7ff947d` |
| [setup-isolated-setup-update](../skills/setup-isolated-setup-update/SKILL.md) | 4,017 | `0ff14e482853f519` |
| [setup-isolated-setup-verify](../skills/setup-isolated-setup-verify/SKILL.md) | 4,562 | `b3f315456ebe2edf` |
| [setup-isolated-setup-verify](../skills/setup-isolated-setup-verify/SKILL.md) | 4,695 | `7a474b088c2b40e5` |
| [setup-override-upstream](../skills/setup-override-upstream/SKILL.md) | 3,681 | `968f51a19ccd4d10` |
| [setup-privacy-llm](../skills/setup-privacy-llm/SKILL.md) | 2,055 | `b89f1f5e421df5cc` |
| [setup-shared-config-sync](../skills/setup-shared-config-sync/SKILL.md) | 3,836 | `d8715462e3c23f38` |
Expand Down
93 changes: 93 additions & 0 deletions docs/setup/sandbox-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
- [Root cause](#root-cause-7)
- [Fix](#fix-7)
- [Notes](#notes-7)
- [`prek` or `uv` not found, or cannot write its cache, inside the sandbox](#prek-or-uv-not-found-or-cannot-write-its-cache-inside-the-sandbox)
- [Symptom](#symptom-8)
- [Root cause](#root-cause-8)
- [Fix](#fix-8)
- [Notes](#notes-8)
- [Adding a new entry](#adding-a-new-entry)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -973,6 +978,94 @@ Two parts:

---

## `prek` or `uv` not found, or cannot write its cache, inside the sandbox

### Symptom

```console
$ prek run --all-files
(eval):1: command not found: prek

$ uv run pytest
(eval):1: command not found: uv
```

A binary reached by absolute path gets further and then fails
writing its cache or state under `~/.cache/` or
`~/.local/share/uv/`.
`git config --global --get <key>` printing nothing inside the
sandbox, while it prints the value in a terminal, is the same
failure on `~/.gitconfig`.

### Root cause

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
`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
`.claude/settings.local.json` or `~/.claude/settings.json` takes
effect. It is the behaviour behind
[issue #197](https://github.com/apache/magpie/issues/197), where the
committed `"."` entry was dropped the same way. The Claude Code
documentation says `allowRead` merges across every scope, so treat
this as harness behaviour that may change, not as a contract.

### Fix

Re-run the project-root helper. It writes the dev-tool paths, as
absolute paths, into the gitignored project-local file, beside the
project root it already adds:

```bash
~/.claude/scripts/sandbox-add-project-root.sh --all-worktrees
```

```jsonc
// <adopter-repo>/.claude/settings.local.json (written by the helper)
{
"sandbox": {
"filesystem": {
"allowRead": [
"/home/<you>/code/<repo>",
"/home/<you>/.gitconfig", // git's user.name / user.email
"/home/<you>/.config/git", // git's per-host config
"/home/<you>/.cache", // uv / prek / ruff / mypy caches
"/home/<you>/.local/share/uv", // uv's tool venvs (prek)
"/home/<you>/.local/bin" // uv-installed entry points
],
"allowWrite": [
"/home/<you>/code/<repo>",
"/home/<you>/.cache",
"/home/<you>/.local/share/uv"
]
}
}
}
```

The helper writes that file only from outside the sandbox (it is in
the harness's write-deny set), and the change applies from the next
session. Confirm with `prek --version` in a fresh session.

### Notes

- The helper deliberately does **not** mirror the whole committed
`allowRead`. That list also names credential paths
(`~/.config/gh/`, `~/.config/apache-magpie/`, `~/.gnupg/`), which
the same harness behaviour currently keeps out of sandboxed Bash.
Re-open one of those only for the tool that needs it, per the
entries above.
- `--no-tool-paths` keeps the old behaviour (project root only) for
an operator who does not run `prek` or `uv` in agent sessions.
- Do not reach for `dangerouslyDisableSandbox: true` to run `prek`:
its hooks execute code from the working tree, and the sandbox is
what keeps a compromised hook away from the rest of `$HOME`.

---

## Adding a new entry

When you hit a sandbox-shaped failure not in this list:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,13 @@ below covers the equivalent).
Run the helper once with `--all-worktrees` in the adopter
repo's main checkout. The helper enumerates
`git worktree list --porcelain` and, for each worktree, writes
that worktree's absolute path into that worktree's own
that worktree's absolute path, plus the absolute dev-tool paths
`prek` and `uv` need, into that worktree's own
`<worktree>/.claude/settings.local.json` (creating the file if
it does not yet exist). Idempotent, atomic, tolerant of missing
it does not yet exist). The dev-tool paths are there because the
harness drops the committed `~/…` allow entries as well; pass
`--no-tool-paths` if the operator does not run `prek` or `uv` in
agent sessions. Idempotent, atomic, tolerant of missing
prereqs (see the script's header comment for the full
failure-mode list). On success, surface the diff so the operator
sees which entries landed; on no-op (paths already present),
Expand Down
2 changes: 2 additions & 0 deletions plugins/magpie-setup/skills/isolated-setup-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ Walk each in order:
- **Static:** confirm the current working tree's absolute path appears in both `sandbox.filesystem.allowRead` and `sandbox.filesystem.allowWrite` of `<worktree>/.claude/settings.local.json`.
For every other linked worktree in `git worktree list --porcelain`, check *that* worktree's own `.claude/settings.local.json`; each worktree carries its own entry.
✗ on any missing entry; remediation: `~/.claude/scripts/sandbox-add-project-root.sh --all-worktrees` (or re-run `setup-isolated-setup-install` if the helper is not installed).
The same file should also carry the absolute dev-tool paths the helper adds (`$HOME/.local/bin`, `$HOME/.local/share/uv`, `$HOME/.cache`, `$HOME/.gitconfig`, `$HOME/.config/git`), because the harness drops the committed `~/…` entries too.
Their absence is ⚠, not ✗: nothing is exposed, but `prek` and `uv` are not found inside the sandbox — [troubleshooting entry](../../../../docs/setup/sandbox-troubleshooting.md#prek-or-uv-not-found-or-cannot-write-its-cache-inside-the-sandbox); same remediation.
- **Live probe:** attempt a sandboxed read of `.git/HEAD` and a sandboxed write of a temp file inside the *current* worktree's project root (e.g. `<root>/.magpie-verify-probe.tmp`, removed right after the write).
The write should succeed because `allowWrite` keeps `.` literal at access time; the read is what exercises the harness bug.
✗ on either failure; remediation as above.
Expand Down
2 changes: 1 addition & 1 deletion tools/agent-isolation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ per runtime — see [`docs/adapters/add-a-harness.md`](../../docs/adapters/add-a
| [`gpg-touch-overlay-window-macos.py`](gpg-touch-overlay-window-macos.py) | The same window on macOS, drawn with Tk — a Mac has neither PyGObject nor zenity, so without this the hook has nothing to show. Main display only, and borderless rather than natively fullscreen so macOS does not switch Spaces out from under the terminal. Takes the keyboard while it is up, so a touch that lands before the key asks for one — which fires the key's OTP slot — types into the overlay instead of whatever was in front. |
| [`container-gateway-hook.sh`](container-gateway-hook.sh) | Claude Code `SessionStart` / `SessionEnd` hook. `start` launches the per-project [container gateway](../container-gateway/) as a detached daemon so sandboxed `podman` / `docker` commands have a policy-checked socket to talk to; `stop` ends it with the session. Finds the gateway in the operator's installed copy (`~/.claude/scripts/container-gateway/src`) or the adopter's `.apache-magpie/` pinned snapshot, and is a silent no-op when neither is present; in-repo copies are never trusted. See [`docs/setup/secure-agent-setup.md` → *Container gateway*](../../docs/setup/secure-agent-setup.md#container-gateway). |
| [`claude-term-bg.sh`](claude-term-bg.sh) | **Opt-in quality-of-life helper (not a security control).** Keeps a calm baseline background and tints it only when Claude genuinely wants you to act (never while working, and never when it merely *finished* a turn), so a window you've tabbed away from can't sit blocked unnoticed. Distinguishes "blocked on a decision" from "finished and idle" — which look identical at the `Stop` event — via three signals across six hooks: `Stop` → `stop` (heuristic — tints only if the final assistant message reads as a question/request; a completion stays calm; needs `python3`/`python`, else defaults calm); `PreToolUse` (matcher `AskUserQuestion`) → `wait` (exact — a structured question was posed); `PostToolUse` (matcher `*`) → `reset` (calm while working, and clears the tint the instant you approve a permission prompt or answer a question); `Notification` → `notify` (tints for permission prompts only — the plain idle ping is a no-op so it can't wipe a pending question's tint); and `UserPromptSubmit` + `SessionStart` → `reset` (you replied / fresh session clears any stale tint). Writes the OSC escape to the Claude pty discovered by walking the process tree (hooks have no controlling tty); the only deterministic reset is an explicit `CLAUDE_RESET_BG` colour via OSC 11 (iTerm2 ignores OSC 111). Colours overridable via `CLAUDE_WAIT_BG` / `CLAUDE_RESET_BG`. Tested on iTerm2 + macOS; fail-soft elsewhere. See [`docs/setup/secure-agent-setup.md` → *Waiting-for-input terminal tint*](../../docs/setup/secure-agent-setup.md#waiting-for-input-terminal-tint). |
| [`sandbox-add-project-root.sh`](sandbox-add-project-root.sh) | Adds the current adopter repo's project root (and, with `--all-worktrees`, every linked git worktree's working dir) as an explicit absolute path to `sandbox.filesystem.allowRead` and `allowWrite` in the project-local, gitignored `<repo>/.claude/settings.local.json` — one entry per worktree, each in that worktree's own settings file. Defensive against [issue #197](https://github.com/apache/magpie/issues/197) — `allowRead: ["."]` does not in practice cover CWD because the harness pre-resolves the `.` literal away from the read side. Never modifies user-scope or committed project-scope. Idempotent, atomic, tolerant of missing prereqs. Invoked from `setup-isolated-setup-install`, `/magpie-setup` (adopt / upgrade / worktree-init), and the `post-checkout` git hook installed by `/magpie-setup adopt`. |
| [`sandbox-add-project-root.sh`](sandbox-add-project-root.sh) | Adds the current adopter repo's project root (and, with `--all-worktrees`, every linked git worktree's working dir) as an explicit absolute path to `sandbox.filesystem.allowRead` and `allowWrite` in the project-local, gitignored `<repo>/.claude/settings.local.json` — one entry per worktree, each in that worktree's own settings file. Defensive against [issue #197](https://github.com/apache/magpie/issues/197) — `allowRead: ["."]` does not in practice cover CWD because the harness pre-resolves the `.` literal away from the read side. The same harness behaviour drops the committed home-directory entries, so it also adds the dev-tool paths `prek` and `uv` need (`~/.gitconfig`, `~/.config/git`, `~/.cache`, `~/.local/share/uv`, `~/.local/bin`; the cache and uv dirs writable) as absolute paths — never the credential paths; `--no-tool-paths` skips them. See [`prek` or `uv` not found inside the sandbox](../../docs/setup/sandbox-troubleshooting.md#prek-or-uv-not-found-or-cannot-write-its-cache-inside-the-sandbox). Never modifies user-scope or committed project-scope. Idempotent, atomic, tolerant of missing prereqs. Invoked from `setup-isolated-setup-install`, `/magpie-setup` (adopt / upgrade / worktree-init), and the `post-checkout` git hook installed by `/magpie-setup adopt`. |
| [`git-global-post-checkout.sh`](git-global-post-checkout.sh) | Universal `post-checkout` git hook installed at `~/.claude/git-hooks/post-checkout` when the operator picks the **simple whole-user** flavour in `setup-isolated-setup-install`. Activated by `git config --global core.hooksPath ~/.claude/git-hooks/` so every `git checkout` / `git clone` / `git worktree add` across the host invokes it. Best-effort + idempotent + `\|\| true`: invokes `sandbox-add-project-root.sh` for any worktree with a `.claude/` directory. Trade-off documented in [`docs/setup/secure-agent-setup.md` → *Per-project vs whole-user scope*](../../docs/setup/secure-agent-setup.md#per-project-vs-whole-user-scope): `core.hooksPath` shadows per-repo `.git/hooks/*` across every repo on the host. The **dispatcher** flavour (below) supersedes this file. |
| [`git-hook-dispatcher.sh`](git-hook-dispatcher.sh) | Universal, basename-keyed git-hook **dispatcher** for the **whole-user dispatcher** flavour. Symlinked to every hook name under `~/.claude/git-hooks/`. For each git operation it runs the framework's own logic for that hook type (the `post-checkout` sandbox-allowlist sync) **and then chains through to the repo-local `.git/hooks/<name>`** (resolved via `git rev-parse --git-common-dir`, worktree-safe; `exec`-ed with original argv + inherited stdin so a failing local hook still aborts the git op). Restores per-repo hooks (prek / pre-commit / husky / hand-written) under global `core.hooksPath`; a repo with no local hook is a clean no-op. See [`docs/setup/secure-agent-setup.md` → *Whole-user with the per-repo dispatcher*](../../docs/setup/secure-agent-setup.md#whole-user-with-the-per-repo-dispatcher). |
| [`prek-shim.sh`](prek-shim.sh) | Transparent `prek` front installed as `~/.claude/bin/prek` (with `~/.claude/bin` prepended to PATH) for the whole-user dispatcher flavour. Rewrites **only** `prek install` — injecting `--git-dir "$(git rev-parse --git-common-dir)"` unless the caller already passed `--git-dir`, asked for `--help`, or is outside a git work tree — so prek's shim lands in the repo-local `.git/hooks/` where the dispatcher chains, instead of the shared `core.hooksPath` dir. Every other `prek` invocation passes through unchanged; no-op on hosts with no global `core.hooksPath`. |
Expand Down
Loading