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
101 changes: 81 additions & 20 deletions docs/setup/secure-agent-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,13 @@ CLAUDE_ISO_ALLOW="GH_TOKEN" GH_TOKEN="$(op read 'op://Personal/GitHub/token')" c
The `CLAUDE_ISO_ALLOW` mechanism is opt-in per invocation — no
implicit propagation, no persistent allowlist.

A session started by an IDE extension (the VS Code extension sets
`CLAUDE_CODE_ENTRYPOINT=claude-vscode`) never goes through
`claude-iso`, so the wrapper strips nothing from its environment.
Such a session relies on `sandbox.credentials` for the variables it
lists, and on you not exporting secrets into the shell that starts
the IDE.

### Automatic sandbox allow-paths

Beyond the env-stripping role, `claude-iso` also injects up to two
Expand Down Expand Up @@ -3570,12 +3577,14 @@ Inside a `claude-iso` session, run these from the agent's Bash
tool. Each should fail or be denied:

```bash
cat ~/.aws/credentials # → permission denied (sandbox)
echo $AWS_ACCESS_KEY_ID # → empty (env stripped by claude-iso)
curl https://example.com # → blocked by permissions.deny
test -r ~/.aws/credentials && echo readable # → nothing (denied or absent; see the 7a sentinel rule)
env | cut -d= -f1 # → names only; no TOKEN/SECRET/*_KEY/*_PAT
curl https://example.com # → blocked by permissions.deny
```

Each command should produce a denial — not a leaked credential.
Never `cat` a credential file to test the denial: if the denial is
broken, the command prints the secret.

### Via a Claude Code prompt

Expand All @@ -3588,13 +3597,31 @@ on its own:
Verify my secure-agent-setup install is complete. Check each item
below and report ✓ done / ✗ missing / ⚠ partial, with the evidence
(file path, line, command output). Do not attempt to fix anything
— surface the gaps and stop:

1. Project `.claude/settings.json` exists and has
— surface the gaps and stop. Never print a credential file's
contents or a credential-shaped variable's value. Before check 1,
locate my framework checkout (this repo if it is the framework,
else an `.apache-magpie/` snapshot in this repo, else the directory
my shell rc sources `agent-iso.sh` from, else ask me; a candidate
counts only if it has `tools/agent-isolation/pinned-versions.toml`);
without one, report 10a, 13c and check 5's `bubblewrap`/`socat`
pins as "unchecked — no framework checkout" and the `claude-code`
floor in check 5 as ⚠ (it cannot be read), still giving my running
`claude --version`. A path that `stat -c '%F %t,%T'`
shows as `character special file 1,3` is a sandbox placeholder, not
a file: take its host state from git or from a read I run, and
never delete it:

1. The merged settings (project `.claude/settings.json`,
project-local `.claude/settings.local.json`, user
`~/.claude/settings.json`, and the managed settings where
readable; name the file each key came from) have
`sandbox.enabled: true`, the `permissions.deny` block, the
`permissions.ask` block, the
`sandbox.network.allowedDomains` block, and the
`sandbox.filesystem` allowlist (`allowRead`/`allowWrite`).
`sandbox.network.allowedDomains` block, the
`sandbox.filesystem` allowlist (`allowRead`/`allowWrite`), and
`sandbox.filesystem.denyRead` containing `~/` (or the resolved
absolute home; ✗ without it: bubblewrap reads are deny-only). A missing committed project file
is not ✗ on its own when the merge passes.
2. User-scope `~/.claude/settings.json` has the `PreToolUse`
`Bash` matcher wired to a `sandbox-bypass-warn.sh` command, a
`PostToolUse` `Bash` matcher wired to a `sandbox-error-hint.sh`
Expand All @@ -3610,17 +3637,39 @@ below and report ✓ done / ✗ missing / ⚠ partial, with the evidence
`~/.claude/scripts/sandbox-status-line.sh`).
4. The `claude-iso` shell function is sourced in `~/.bashrc` or
`~/.zshrc`. Note whether `alias claude='claude-iso'` is set.
5. The pinned sandbox primitives from
5. The pinned sandbox primitives from the checkout's
`tools/agent-isolation/pinned-versions.toml` are installed at
the pinned versions: `bubblewrap` (Linux only), `socat`
(Linux only). For the agent harness `claude-code` (unpinned,
`@latest`), instead compare my running `claude --version`
against the `[tools.claude-code]` `min_version` floor —
**hard-fail** if below it (running under Claude Code), else ✓.
**hard-fail** if below it (running under Claude Code), ⚠ when the
floor cannot be read, else ✓.
6. The status-line prefix in this session shows `[sandbox]` (not
`[NO SANDBOX]`).
7. Run `cat ~/.aws/credentials`, `echo $AWS_ACCESS_KEY_ID`, and
`curl https://example.com` and confirm each is denied.
7. Denials, each as its own Bash call. (a) `test -r` each of
`~/.aws/credentials`, the `~/.ssh/id_*` private keys (list the readable ones with
`find -H ~/.ssh -maxdepth 1 -name 'id_*' ! -name '*.pub' -exec test -r {} \; -print`,
which behaves the same in bash and zsh and follows a symlinked
`~/.ssh`; its error separates absent from denied except under
bubblewrap; then `stat` each),
`~/.config/gh/hosts.yml`, `~/.netrc`, `~/.docker/config.json`:
any readable one is ✗ unless it is a sandbox placeholder. With
`denyRead` covering `~/`, prove the denial with a sentinel: a file
under `~/` that exists on my host (such as my shell rc) and that
no `allowRead` entry covers; a sandboxed `test -r` that fails
(not `-e`, which zsh on macOS can pass) is ✓, and a `/dev/null`
placeholder counts as denied; a readable non-placeholder sentinel
is ✗. With no sentinel, report "inconclusive" and ask me to
`ls` the candidates from my own terminal; 7a turns ✓ once my `ls`
shows a file the sandbox cannot see. (b) `env | cut -d= -f1`:
names only; any credential-shaped name (TOKEN, SECRET, PASSWORD,
PASSWD, CREDENTIAL, DSN, API_KEY, a `_`-delimited AUTH segment,
`*_KEY`, `*_PAT`, `AWS_*`, `GH_*`, `GITHUB_*`, `AZURE_*`) outside
the `agent-iso.sh` passthrough list is ⚠ (ask me whether I opted
it in with `AGENT_ISO_ALLOW`), and so is a `CLAUDE_CODE_ENTRYPOINT` of an
IDE extension (such as `claude-vscode`), which never went through
`claude-iso`. (c) `curl https://example.com` is denied.
8. **Project-root coverage in the sandbox allowlists.** For this
worktree and every other one `git worktree list --porcelain`
names, confirm the worktree's own absolute path is in that
Expand Down Expand Up @@ -3659,8 +3708,22 @@ below and report ✓ done / ✗ missing / ⚠ partial, with the evidence
report it as cruft to remove, not as a second layer.
If the repo has no vetted-ops policy at all, report n/a.
The read dispatcher's never-prompt shape is check 16.
10. If a hardware key signs my commits or authenticates my git
remotes: the touch overlay is wired (`PreToolUse` /
10. If my commits are signed with `gpg.format=ssh`: the file
`git config user.signingkey` names is readable from a sandboxed
Bash (it needs its own `sandbox.filesystem.allowRead` entry).
If a hardware key signs my commits or authenticates my git
remotes (an `sk-` public key, or a card holding the signing key or the
key a remote authenticates with: for ssh format the `ssh-add -L`
line equal to `user.signingkey` has a `cardno:` comment; for
OpenPGP `gpg --list-secret-keys --with-colons <key>` (`<key>` is
`user.signingkey`, or the committer email when unset) shows a card
serial in field 15 (not `+` or `#`) on a signing-capable record
(lowercase `s` in field 12), or
`gpg --card-status`'s signature key matches; for a gpg-agent ssh
remote the offered key has a `cardno:` comment; a card with only
unrelated keys, or only the auth or encryption subkey for
OpenPGP signing, does not count; n/a otherwise): the touch overlay
is wired (`PreToolUse` /
`PostToolUse` `Bash` →
`~/.claude/scripts/gpg-touch-overlay.sh arm` / `disarm`), its
scripts match the framework's `tools/agent-isolation/` copies,
Expand All @@ -3675,10 +3738,7 @@ below and report ✓ done / ✗ missing / ⚠ partial, with the evidence
the key's signing slot carries a touch policy and — with
OpenPGP signing — its authentication slot does not
(`ykman openpgp info`, which I run myself; with
`gpg.format=ssh` the `aut` slot is the signing slot),
and — with `gpg.format=ssh` — the file `git config
user.signingkey` names is readable from a sandboxed Bash (it
needs its own `sandbox.filesystem.allowRead` entry). For the
`gpg.format=ssh` the `aut` slot is the signing slot). For the
toolkit probe (`gpg-touch-overlay.sh _gui_available`) hand me
the command to run myself: it cannot see the display from
inside the sandbox.
Expand Down Expand Up @@ -3981,8 +4041,9 @@ anything — I will decide what to apply:
`sandbox.excludedCommands` entry, and any missing vetted-ops
`Edit` deny rule. See
[Read-only vetted ops never prompt](#read-only-vetted-ops-never-prompt).
6. Re-run `cat ~/.aws/credentials`, `echo $AWS_ACCESS_KEY_ID`,
`curl https://example.com` and confirm each is still denied.
6. Re-run check 7's probes (7a `test -r` with the sentinel, 7b
variable names only, 7c `curl https://example.com`) and confirm
each is still denied.
Note any newly-allowed call as a regression to investigate.
```

Expand Down
5 changes: 4 additions & 1 deletion plugins/magpie-setup/skills/isolated-setup-doctor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ capability:
- capability:reassess
surface_hash: sha256:7d670b572d43bbeb
license: Apache-2.0
measured_tokens: 6611
measured_tokens: 6745
---

<!-- Placeholder convention (see AGENTS.md#placeholder-convention-used-in-skill-files):
Expand Down Expand Up @@ -73,6 +73,9 @@ If a fail shows a failure mode not catalogued there, propose appending a new ent
⊘ means the probe was skipped because a prerequisite is absent (e.g. no `docker` / `podman` on `PATH` → docker probe ⊘, not ✗).
- **Surface evidence.** Each report line names the probe command, the exit code, and the relevant stderr snippet.
"Looks blocked" is not useful; "ssh-add -l → rc=2 → `Could not open a connection to your authentication agent`" is.
- **Sandbox placeholders are not files.** Inside the Claude Code sandbox a denied path that does not exist on the host can appear as a `/dev/null` bind mount: `stat -c '%F %t,%T' <path>` prints `character special file 1,3`.
`git status` lists such placeholders as untracked (`.bash_profile`, `.claude/settings.json`, `.mcp.json`), and reading one returns nothing.
Take the host state from git (`git ls-files <path>`, `git log -- <path>`) or from a read the user runs outside the sandbox, and never delete the placeholder.
- **Map each ✗ to a catalog entry.** The fail report links directly to the matching section of [`docs/setup/sandbox-troubleshooting.md`](../../../../docs/setup/sandbox-troubleshooting.md).
Do not paraphrase the remediation; the catalog is the single source of truth.

Expand Down
Loading