Skip to content

Commit 20df286

Browse files
committed
fix(setup): address review of isolated-setup-verify checks
Check 10 counted a key as hardware only for an sk- public key or an OpenPGP-format card. ssh-format signing through gpg-agent uses the card's aut slot and shows a plain ssh-ed25519 or ssh-rsa key, so a card gpg --card-status finds now counts whatever gpg.format is. Check 7a was inconclusive on every correctly secured host, because with denyRead covering ~/ every path under it looks missing. It now proves the denial with a sentinel: a file known to exist on the host, such as the shell rc, that the sandbox cannot see. Inconclusive is left for the case with no sentinel. The step-1 output spec gains the inconclusive and unchecked verdicts the skill now emits, with cases for both and a step-2 case for the unchecked routing. The rest is review follow-up: managed settings in check 1's merge, the opt-in question for flagged variable names, the running claude-code version beside an unchecked check 5, the credential-shaped wording of the no-print rule, the update prompt's denial probes, and probe output attributed to stat in the fixtures. Generated-by: Claude Code (Claude Opus 5.5)
1 parent 9a08fdf commit 20df286

23 files changed

Lines changed: 285 additions & 52 deletions

File tree

‎docs/setup/secure-agent-setup.md‎

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3516,18 +3516,21 @@ Verify my secure-agent-setup install is complete. Check each item
35163516
below and report ✓ done / ✗ missing / ⚠ partial, with the evidence
35173517
(file path, line, command output). Do not attempt to fix anything
35183518
— surface the gaps and stop. Never print a credential file's
3519-
contents or an environment variable's value. Before check 1,
3520-
locate my framework checkout (an `.apache-magpie/` snapshot in this
3521-
repo, else the directory my shell rc sources `agent-iso.sh` from,
3522-
else ask me); without one, report checks 5, 10a and 13c as
3523-
"unchecked — no framework checkout". A path that `stat -c '%F %t,%T'`
3519+
contents or a credential-shaped variable's value. Before check 1,
3520+
locate my framework checkout (this repo if it is the framework,
3521+
else an `.apache-magpie/` snapshot in this repo, else the directory
3522+
my shell rc sources `agent-iso.sh` from, else ask me; a candidate
3523+
counts only if it has `tools/agent-isolation/pinned-versions.toml`);
3524+
without one, report checks 5, 10a and 13c as "unchecked — no
3525+
framework checkout", still giving my running `claude --version`. A path that `stat -c '%F %t,%T'`
35243526
shows as `character special file 1,3` is a sandbox placeholder, not
35253527
a file: take its host state from git or from a read I run, and
35263528
never delete it:
35273529
35283530
1. The merged settings (project `.claude/settings.json`,
35293531
project-local `.claude/settings.local.json`, user
3530-
`~/.claude/settings.json`; name the file each key came from) have
3532+
`~/.claude/settings.json`, and the managed settings where
3533+
readable; name the file each key came from) have
35313534
`sandbox.enabled: true`, the `permissions.deny` block, the
35323535
`permissions.ask` block, the
35333536
`sandbox.network.allowedDomains` block, the
@@ -3562,12 +3565,17 @@ never delete it:
35623565
7. Denials, each as its own Bash call. (a) `test -r` each of
35633566
`~/.aws/credentials`, `~/.ssh/id_*` private keys,
35643567
`~/.config/gh/hosts.yml`, `~/.netrc`, `~/.docker/config.json`:
3565-
any readable one is ✗ unless it is a sandbox placeholder; if
3566-
none is visible, report "inconclusive" and ask me to `ls` them
3567-
from my own terminal. (b) `env | cut -d= -f1`: names only; any
3568-
credential-shaped name (TOKEN, SECRET, PASSWORD, API_KEY, `*_KEY`,
3569-
`AWS_*`, `GH_*`, `GITHUB_*`, `AZURE_*`) outside the `agent-iso.sh`
3570-
passthrough list is ⚠, and so is a `CLAUDE_CODE_ENTRYPOINT` of an
3568+
any readable one is ✗ unless it is a sandbox placeholder. With
3569+
`denyRead` covering `~/`, prove the denial with a sentinel: a file
3570+
under `~/` that exists on my host (such as my shell rc) and that
3571+
no `allowRead` entry covers; a sandboxed `test -e` that cannot see
3572+
it is ✓. With no sentinel, report "inconclusive" and ask me to
3573+
`ls` the candidates from my own terminal; 7a turns ✓ once my `ls`
3574+
shows a file the sandbox cannot see. (b) `env | cut -d= -f1`:
3575+
names only; any credential-shaped name (TOKEN, SECRET, PASSWORD,
3576+
API_KEY, `*_KEY`, `AWS_*`, `GH_*`, `GITHUB_*`, `AZURE_*`) outside
3577+
the `agent-iso.sh` passthrough list is ⚠ (ask me whether I opted
3578+
it in with `AGENT_ISO_ALLOW`), and so is a `CLAUDE_CODE_ENTRYPOINT` of an
35713579
IDE extension (such as `claude-vscode`), which never went through
35723580
`claude-iso`. (c) `curl https://example.com` is denied.
35733581
8. **Project-root coverage in the sandbox allowlists.** For this
@@ -3916,8 +3924,9 @@ anything — I will decide what to apply:
39163924
`sandbox.excludedCommands` entry, and any missing vetted-ops
39173925
`Edit` deny rule. See
39183926
[Read-only vetted ops never prompt](#read-only-vetted-ops-never-prompt).
3919-
6. Re-run `cat ~/.aws/credentials`, `echo $AWS_ACCESS_KEY_ID`,
3920-
`curl https://example.com` and confirm each is still denied.
3927+
6. Re-run check 7's probes (7a `test -r` with the sentinel, 7b
3928+
variable names only, 7c `curl https://example.com`) and confirm
3929+
each is still denied.
39213930
Note any newly-allowed call as a regression to investigate.
39223931
```
39233932

‎plugins/magpie-setup/skills/isolated-setup-verify/SKILL.md‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ when_to_use: >-
1818
capability: capability:platform
1919
surface_hash: sha256:ce33cb9437d44992
2020
license: Apache-2.0
21-
measured_tokens: 8586
21+
measured_tokens: 8836
2222
---
2323

2424
<!-- Placeholder convention (see AGENTS.md#placeholder-convention-used-in-skill-files):
@@ -84,7 +84,7 @@ Local modifications go in the override file; framework changes go via PR to `apa
8484
The doc-allowed "richer custom statusLine" that embeds the framework's sandbox-prefix logic in a larger script is ⚠ (the by-name helper is absent, but the functionality is there).
8585
Use ⚠ for any *intentional* variation from the doc default; ✗ only for genuine gaps.
8686
- **Surface evidence.** Each check's report line names the file path, version string, command output, or `sandbox.enabled` value; never just "✓" or "✗" alone.
87-
- **Never print a credential.** No probe may print a credential file's contents or an environment variable's value: probe files with `test -r`, never `cat` or `head`, and list variables by name only.
87+
- **Never print a credential.** No probe may print a credential file's contents or a credential-shaped variable's value: probe files with `test -r`, never `cat` or `head`, and list variables by name only.
8888
- **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`.
8989
`git status` lists such placeholders as untracked (`.bash_profile`, `.claude/settings.json`, `.mcp.json`), and reading one returns nothing, so an empty settings file seen this way is not evidence for checks 1 or 6.
9090
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.
@@ -100,9 +100,10 @@ Look, in order, for: the current repository when it is the framework itself; an
100100
A candidate counts only when it has `tools/agent-isolation/pinned-versions.toml`; a copy under `~/.claude/agent-isolation/` is not a checkout.
101101
With none found, ask the user for the path once.
102102
With no checkout at all, report check 5's version comparisons, 10a's script comparison and 13c as **unchecked — no framework checkout**, not ✗ or ⚠, and say which path to supply.
103+
Check 5 still reports the running `claude --version` beside that verdict.
103104

104105
1. Effective settings shape: `sandbox.enabled: true`, `permissions.deny`, `permissions.ask`, `sandbox.network.allowedDomains`, the `sandbox.filesystem` allowlist (`allowRead`/`allowWrite`), and `sandbox.filesystem.denyRead` containing `~/` (or the resolved absolute home).
105-
Evaluate the merge of the project `.claude/settings.json`, the project-local `.claude/settings.local.json` and the user-scope `~/.claude/settings.json`, and name the file each key came from.
106+
Evaluate the merge of the project `.claude/settings.json`, the project-local `.claude/settings.local.json`, the user-scope `~/.claude/settings.json` and, where readable, the managed settings (`/etc/claude-code/managed-settings.json` and `managed-settings.d/` on Linux, `/Library/Application Support/ClaudeCode/` on macOS, `C:\Program Files\ClaudeCode\` on Windows), and name the file each key came from.
106107
A missing `denyRead` home entry is ✗: Linux bubblewrap reads are deny-only, so without it every credential file under `~/` stays readable to sandboxed commands (`tools/sandbox-lint` enforces the same rule).
107108
A missing committed project `.claude/settings.json` is not ✗ on its own when the merged settings pass; say which file carries them.
108109
2. User-scope `~/.claude/settings.json` wiring: `PreToolUse` `Bash` matcher → `sandbox-bypass-warn.sh`, `PostToolUse` `Bash` matcher → `sandbox-error-hint.sh`, `statusLine` → `sandbox-status-line.sh` (or a custom statusline script that embeds the framework's prefix logic, the doc-allowed variant; report ⚠).
@@ -138,10 +139,14 @@ With no checkout at all, report check 5's version comparisons, 10a's script comp
138139
- **7a — credential files.** For each of `~/.aws/credentials`, the `~/.ssh/id_*` private keys (not `*.pub`), `~/.config/gh/hosts.yml`, `~/.netrc` and `~/.docker/config.json`, run `test -r <path>` and `stat` it (`stat -c '%F %t,%T'` on Linux, `stat -f %HT` on macOS).
139140
The first one the sandbox reports readable is ✗, unless `stat` prints `character special file 1,3` (a sandbox placeholder, which counts as denied).
140141
`Operation not permitted` from `stat` (Seatbelt) is a denial of a file that exists: ✓.
141-
Under bubblewrap a denied file and a missing one look the same, so when no candidate is visible report 7a as **inconclusive**, not ✓, and ask the user to `ls` the candidates from their own terminal; ✓ once one of them exists on the host.
142+
Under bubblewrap a denied file and a missing one look the same, so a candidate that is not visible proves nothing on its own.
143+
When check 1 found `denyRead` covering `~/`, prove the denial with a sentinel: a file under `~/` known to exist on the host (such as the shell rc check 4 read) that no `allowRead` entry covers.
144+
A sandboxed `test -e <sentinel>` that fails proves the denial: ✓.
145+
Only with no sentinel available is 7a **inconclusive**: ask the user to `ls` the candidates from their own terminal, and report ✓ once one exists there that the sandbox cannot see.
142146
- **7b — clean environment.** List the variable names, never the values: `env | cut -d= -f1 | sort`.
143147
Flag every credential-shaped name (containing `TOKEN`, `SECRET`, `PASSWORD` or `API_KEY`, ending in `_KEY`, or starting with `AWS_`, `GH_`, `GITHUB_` or `AZURE_`) that is not on the `agent-iso.sh` passthrough list; any such name is ⚠, listed by name.
144148
A single empty variable proves nothing: the reference `sandbox.credentials` already unsets `AWS_ACCESS_KEY_ID` for sandboxed commands, wrapper or not.
149+
An `AGENT_ISO_ALLOW` / `CLAUDE_ISO_ALLOW` opt-in is invisible from inside the session, so ask the user whether a flagged name was opted in before calling it a gap.
145150
Report `CLAUDE_CODE_ENTRYPOINT`.
146151
An IDE extension's value (such as `claude-vscode`) means the session never went through `claude-iso`, so no wrapper stripped its environment: ⚠, with the credential-shaped names found or "none found" ([why](../../../../docs/setup/secure-agent-setup.md#the-clean-env-wrapper)).
147152
- **7c — network.** `curl https://example.com` should deny at the permission-prompt layer (`Permission to use Bash with command curl … has been denied`).
@@ -175,7 +180,9 @@ With no checkout at all, report check 5's version comparisons, 10a's script comp
175180

176181
10. **Hardware-key touch overlay and the signing key.** Only when commits are signed (`git config --get commit.gpgsign` is `true`) or a remote is reached over ssh through gpg-agent (`SSH_AUTH_SOCK` names its socket); otherwise **n/a**.
177182
10c applies to any ssh-format signing key.
178-
10a, 10b, 10d and 10e apply only to a hardware key and are **n/a** otherwise: with `gpg.format=ssh` the public key line starts with `sk-` (`sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com`); with OpenPGP, `gpg --card-status` finds a card (the user runs it when the sandbox cannot see the device).
183+
10a, 10b, 10d and 10e apply only to a hardware key and are **n/a** otherwise.
184+
A hardware key is an `sk-` public key (`sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com`), or a card `gpg --card-status` finds, whatever `gpg.format` is: ssh-format signing through gpg-agent uses the card's `aut` slot and shows a plain `ssh-ed25519` or `ssh-rsa` key.
185+
The user runs `gpg --card-status` when the sandbox cannot see the device.
179186
Five sub-checks:
180187
[`conditional-checks.md`](conditional-checks.md#check-10--hardware-key-touch-overlay-and-the-signing-key).
181188

@@ -291,8 +298,8 @@ If anything is ✗ or ⚠, suggest the matching follow-up skill without invoking
291298

292299
- ✗ on checks 1 / 2 / 3 / 4 → `setup-isolated-setup-install` (missing install pieces).
293300
For a missing `denyRead` home entry, name the settings file to add it to.
294-
- **Unchecked — no framework checkout** on check 5, 10a or 13c → not a failure; ask the user for their `magpie` clone's path, or to clone one ([install item 2](../isolated-setup-install/SKILL.md)), and re-run.
295-
- Inconclusive 7a → the user runs `ls` on the candidate credential paths from their own terminal; one that exists there while the sandbox cannot see it is ✓.
301+
- **Unchecked — no framework checkout** on check 5, 10a or 13c → not a failure; re-run `setup-isolated-setup-verify` with the path of the user's `magpie` clone, cloning one first if they have none ([install item 2](../isolated-setup-install/SKILL.md)).
302+
- Inconclusive 7a (no sentinel) → the user runs `ls` on the candidate credential paths from their own terminal; one that exists there while the sandbox cannot see it is ✓.
296303
- ⚠ on check 7b (an IDE-launched session, or credential-shaped names in the environment) → no skill fixes it: start the session through `claude-iso` from a terminal, or keep secrets out of the shell that starts the IDE and rely on `sandbox.credentials` ([The clean-env wrapper](../../../../docs/setup/secure-agent-setup.md#the-clean-env-wrapper)).
297304
- **✗ on check 5 (claude-code below the `min_version` floor, running under Claude Code)** → **hard fail; stop.**
298305
Tell the operator to upgrade (`npm install -g --no-save @anthropic-ai/claude-code@latest`) and re-run; the setup cannot be certified on a below-floor runtime.

‎plugins/magpie-setup/skills/isolated-setup-verify/conditional-checks.md‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,15 @@ reading this file; read the section for a check whose condition holds.
160160
`true`) or a remote is reached over ssh through gpg-agent
161161
(`SSH_AUTH_SOCK` names its socket); otherwise report **n/a**.
162162
10c applies to any ssh-format signing key. 10a, 10b, 10d and 10e
163-
apply only to a hardware key and are **n/a** otherwise: with
164-
`gpg.format=ssh` the public key line starts with `sk-`
165-
(`sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com`);
166-
with OpenPGP, `gpg --card-status` finds a card (the user runs it
167-
when the sandbox cannot see the device). A plain `ssh-ed25519` key
168-
needs no touch, so the overlay has nothing to show.
163+
apply only to a hardware key and are **n/a** otherwise. A
164+
hardware key is an `sk-` public key (`sk-ssh-ed25519@openssh.com`,
165+
`sk-ecdsa-sha2-nistp256@openssh.com`), or a card
166+
`gpg --card-status` finds, whatever `gpg.format` is: ssh-format
167+
signing through gpg-agent uses the card's `aut` slot and shows a
168+
plain `ssh-ed25519` or `ssh-rsa` key. The user runs
169+
`gpg --card-status` when the sandbox cannot see the device. A
170+
plain software key needs no touch, so the overlay has nothing to
171+
show.
169172
Five sub-checks, plus a note for a hardware key: the key's own touch policies
170173
(`ykman openpgp info`, run by the user — the sandbox does not
171174
see the device) are what make the overlay matter; report them

‎tools/skill-evals/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Suites are currently implemented for:
3535
- **pr-management-stats** — 6 cases across 1 step (build-loop)
3636
- **pr-management-triage** — 33 cases across 6 steps (backport-check, classify-loop, deliver-note, guard-reroute, terminal-links, interaction-progress); the pre-filters, decision table and pagination are pytest-tested in `tools/pr-management`
3737
- **list-skills** — 8 cases across 2 steps (step-1-command, step-2-present)
38-
- **setup-isolated-setup-verify** — 31 cases across 4 steps (runtime-routing, codex-native, step-1-classify, step-2-recommend)
38+
- **setup-isolated-setup-verify** — 34 cases across 4 steps (runtime-routing, codex-native, step-1-classify, step-2-recommend)
3939
- **setup-isolated-setup-update** — 15 cases across 4 steps (runtime-routing, step-snapshot-drift, step-tool-freshness, step-after-report)
4040
- **setup-isolated-setup-doctor** — 26 cases across 3 steps (runtime-routing, interpret-probes, after-report)
4141
- **contributor-activity-sweep** — 13 cases across 3 steps (step-0-resolve-inputs, step-1-classify-reviews, step-2-render)

‎tools/skill-evals/evals/setup-isolated-setup-verify/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
Behavioral evals for the `setup-isolated-setup-verify` skill.
77

8-
## Suites (31 cases total)
8+
## Suites (34 cases total)
99

1010
| Suite | Step | Cases | What it covers |
1111
|---|---|---|---|
1212
| codex-native | Codex security model | 3 | untrusted or absent guard despite project trust, unsafe vetted write allowance, and the credential read-isolation gap |
1313
| runtime-routing | Runtime routing | 2 | Codex and Gemini route to their native adapters and never require Claude files |
14-
| step-1-classify | The 12 checks | 20 | all-pass, sandbox disabled, missing scripts, version drift, project root missing, injection attempt, signing key unreadable in the sandbox (check 10), dev-tool paths missing from settings.local.json (check 8 ⚠), whole-user hook dir unreadable from the sandbox (check 8 ✗), working directories missing under the read block with a glob entry (check 15 ⚠), roborev installed with `review` and `show` auto-allowed no config-edit deny and an incomplete `ask` list (check 17b, 17d and 17e ✗), roborev's hook replacing the shared whole-user dispatcher (check 17f ✗), `~/.roborev/` made writable with and without step B.2 (check 17c ✗), a roborev MCP server registered (check 17g ⚠), the vetted-ops checkout listed as a working directory (check 16f ✗), `denyRead` missing from every merged scope (check 1 ✗), settings only in `settings.local.json` beside a sandbox placeholder (check 1 ✓ naming the file), an IDE-launched session with credential-shaped variable names (check 7 ⚠), a plain `ssh-ed25519` signing key with `commit.gpgsign` on (check 10 ✓, the overlay sub-checks n/a) |
15-
| step-2-recommend | After the report | 6 | all-pass, install needed, update needed, project-root missing, multiple gaps, roborev findings (settings edit by the operator, `.roborev.toml` left to them) |
14+
| step-1-classify | The 12 checks | 22 | all-pass, sandbox disabled, missing scripts, version drift, project root missing, injection attempt, signing key unreadable in the sandbox (check 10), dev-tool paths missing from settings.local.json (check 8 ⚠), whole-user hook dir unreadable from the sandbox (check 8 ✗), working directories missing under the read block with a glob entry (check 15 ⚠), roborev installed with `review` and `show` auto-allowed no config-edit deny and an incomplete `ask` list (check 17b, 17d and 17e ✗), roborev's hook replacing the shared whole-user dispatcher (check 17f ✗), `~/.roborev/` made writable with and without step B.2 (check 17c ✗), a roborev MCP server registered (check 17g ⚠), the vetted-ops checkout listed as a working directory (check 16f ✗), `denyRead` missing from every merged scope (check 1 ✗), settings only in `settings.local.json` beside a sandbox placeholder (check 1 ✓ naming the file), an IDE-launched session with credential-shaped variable names (check 7 ⚠), a plain `ssh-ed25519` signing key with `commit.gpgsign` on (check 10 ✓, the overlay sub-checks n/a), no sentinel and no `ls` for 7a (check 7 inconclusive), no framework checkout (checks 5, 10 and 13 unchecked, installed versions still reported) |
15+
| step-2-recommend | After the report | 7 | all-pass, install needed, update needed, project-root missing, multiple gaps, roborev findings (settings edit by the operator, `.roborev.toml` left to them), no framework checkout (pass, with a re-run once a clone exists) |
1616

1717
## Run
1818

‎tools/skill-evals/evals/setup-isolated-setup-verify/step-1-classify/fixtures/case-1-all-pass/report.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ Effective sandbox.enabled: true
120120
## Check 7 — Denial commands
121121

122122
test -r <path>, for each of ~/.aws/credentials, ~/.ssh/id_ed25519, ~/.config/gh/hosts.yml, ~/.netrc, ~/.docker/config.json:
123-
none readable — No such file or directory for every path
123+
rc=1 for every path
124+
125+
stat -c '%F %t,%T' <path>, for each of the same paths:
126+
stat: cannot statx '<path>': No such file or directory
124127

125128
ls ~/.aws/credentials ~/.ssh/id_ed25519 (run by the user from their own terminal):
126129
/home/alice/.aws/credentials

0 commit comments

Comments
 (0)