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: 0 additions & 4 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"env": {
"CONTAINER_HOST": "unix://./.apache-magpie-local/run/podman.sock",
"DOCKER_HOST": "unix://./.apache-magpie-local/run/docker.sock"
},
"sandbox": {
"enabled": true,
"excludedCommands": [
Expand Down
10 changes: 5 additions & 5 deletions docs/mode-economics.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ history separately provides its publication revision and date.

<!-- BEGIN GENERATED SKILL TOKEN COUNTS -->

Measured on (UTC): 2026-09-20.
Measured on (UTC): 2026-09-21.

Tokenizer: **tiktoken 0.14.0, `cl100k_base`**. Method: full UTF-8 file,
including frontmatter and comments; line endings normalized to LF;
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: `f44def0e67cfebd4dceadc4b3253f16215740b0f611736356b3f0f300bb3b2f3`.
Measurement manifest SHA-256: `025b473aca2cddcd3304dfc5adbda35c972e51c0039ad790437528e3e047060c`.

| Skill file | Measured tokens | Source SHA-256 (first 16 characters) |
|---|---:|---|
Expand Down Expand Up @@ -159,10 +159,10 @@ Measurement manifest SHA-256: `f44def0e67cfebd4dceadc4b3253f16215740b0f611736356
| [security-model-verify](../skills/security-model-verify/SKILL.md) | 6,625 | `cde155672857b33c` |
| [security-tracker-stats-dashboard](../skills/security-tracker-stats-dashboard/SKILL.md) | 4,897 | `b52154deb8557ba4` |
| [setup](../skills/setup/SKILL.md) | 8,724 | `82788542bb240309` |
| [setup-isolated-setup-doctor](../skills/setup-isolated-setup-doctor/SKILL.md) | 7,651 | `7485a409d69de376` |
| [setup-isolated-setup-install](../skills/setup-isolated-setup-install/SKILL.md) | 10,059 | `d2a1e98a9f2c3b38` |
| [setup-isolated-setup-doctor](../skills/setup-isolated-setup-doctor/SKILL.md) | 7,950 | `3fa5d728fa080ed0` |
| [setup-isolated-setup-install](../skills/setup-isolated-setup-install/SKILL.md) | 10,110 | `c32091305674dd2b` |
| [setup-isolated-setup-update](../skills/setup-isolated-setup-update/SKILL.md) | 4,843 | `60bab0e30ac2e25a` |
| [setup-isolated-setup-verify](../skills/setup-isolated-setup-verify/SKILL.md) | 7,410 | `e5c4cd00e97147ad` |
| [setup-isolated-setup-verify](../skills/setup-isolated-setup-verify/SKILL.md) | 7,489 | `d7fa414b358d90ad` |
| [setup-override-upstream](../skills/setup-override-upstream/SKILL.md) | 4,012 | `fb583feb56b7f77c` |
| [setup-privacy-llm](../skills/setup-privacy-llm/SKILL.md) | 2,145 | `0e27b542a1656846` |
| [setup-shared-config-sync](../skills/setup-shared-config-sync/SKILL.md) | 4,357 | `d1dfcd7cdeb5f5a6` |
Expand Down
72 changes: 42 additions & 30 deletions docs/setup/sandbox-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,20 +614,25 @@ See [Container gateway](secure-agent-setup.md#container-gateway) in the setup gu

| Error line | Cause | Action |
|---|---|---|
| `failed to read identity "…/machine/machine": operation not permitted` | `CONTAINER_HOST` / `DOCKER_HOST` are unset, so the CLI fell back to its default connection instead of the gateway | Add the reference `env` block below to `.claude/settings.json` / `settings.local.json` |
| `dial unix ./.apache-magpie-local/run/podman.sock: connect: no such file or directory` | The gateway is not running for this project | Run `~/.claude/scripts/container-gateway-hook.sh start` from a terminal, or check `<project>/.apache-magpie-local/run/container-gateway.log` for why it did not start |
| `dial unix ./.apache-magpie-local/run/podman.sock: connect: operation not permitted` | The gateway is running but its socket is missing from `sandbox.network.allowUnixSockets` | Add both gateway sockets as absolute paths, per [Container gateway](secure-agent-setup.md#container-gateway) |
| `failed to read identity "…/machine/machine": operation not permitted` | `CONTAINER_HOST` / `DOCKER_HOST` are unset, so the CLI fell back to its default connection instead of the gateway | Add the reference `env` block below to `.claude/settings.local.json` |
| `dial unix /.//.apache-magpie-local/run/podman.sock` — note the leading `/.//` | `CONTAINER_HOST` / `DOCKER_HOST` use a project-relative `unix://./…` value, which the CLIs do **not** resolve against the cwd | Use the absolute `unix:///<project>/…` spelling in the `env` block below |
| `dial unix /<project>/.apache-magpie-local/run/podman.sock: connect: no such file or directory` | The gateway is not running for this project | Run `~/.claude/scripts/container-gateway-hook.sh start` from a terminal, or check `<project>/.apache-magpie-local/run/container-gateway.log` for why it did not start |
| `dial unix /<project>/.apache-magpie-local/run/podman.sock: connect: operation not permitted` | The gateway is running but its socket is missing from `sandbox.network.allowUnixSockets` | Add both gateway sockets as absolute paths, per [Container gateway](secure-agent-setup.md#container-gateway) |
| `no podman or docker backend found; nothing to serve` in the gateway log, while `podman` works by hand | On macOS the gateway asked `podman machine inspect` for the socket path, and that command renders it from the **caller's** `TMPDIR` | Update the framework: discovery now also probes `getconf DARWIN_USER_TEMP_DIR`/`podman/`, so a hook whose `TMPDIR` differs from the machine's still finds the socket |

```jsonc
// .claude/settings.json (already the framework's committed default on this branch)
// .claude/settings.local.json (gitignored, per machine — NOT committed)
{
"env": {
"CONTAINER_HOST": "unix://./.apache-magpie-local/run/podman.sock",
"DOCKER_HOST": "unix://./.apache-magpie-local/run/docker.sock"
"CONTAINER_HOST": "unix:///<project>/.apache-magpie-local/run/podman.sock",
"DOCKER_HOST": "unix:///<project>/.apache-magpie-local/run/docker.sock"
}
}
```

A `unix://` URL's authority is parsed as a host component, so every relative spelling misses the socket — `unix://./x` dials `/.//x`, `unix://x` dials `/x/`, and `unix:x` dials `//`.
`unix:///absolute/path` is the only form that connects (verified against podman 6.1.0), which is why this block is per-machine rather than committed.

#### `403 container-gateway: …`

A request that reaches the gateway but fails its policy comes back as `403`, and the CLI prints the message verbatim, for example `container-gateway: bind-mount: /Users/you/.ssh is outside the allowed roots (…); see docs/setup/sandbox-troubleshooting.md#docker--podman-command-fails-with-a-socket-error`.
Expand Down Expand Up @@ -696,43 +701,50 @@ directory and can collide on identical filenames.

### Fix

Point `TMPDIR` at a per-project directory inside the writable tree,
in the **project's** `.claude/settings.local.json` — that file is
per-project, so the value is per-project by construction:
For the **unset / overwritten** and **outside `allowWrite`** cases
above, point `TMPDIR` back at a directory inside the writable tree
for whatever cleared it — the `env -i` wrapper, the Makefile, the
login shell — at that call site. `/tmp/claude-<uid>/` is already
inside the sandbox's writable set, so nothing needs widening.

For the **shared-session-root** case there is currently **no fix**.
Setting `env.TMPDIR` in the project's `.claude/settings.local.json`
— which this entry recommended until recently — does not work:

```jsonc
// <adopter-repo>/.claude/settings.local.json
{
"env": {
// <uid> is your numeric uid; <path-slug> is the project's
// absolute path with "/" replaced by "-".
// Accepted, and silently without effect. Do not rely on it.
"TMPDIR": "/tmp/claude-<uid>/<path-slug>/shared"
}
}
```

Per-entry rationale:

- `/tmp/claude-<uid>/` is already inside the sandbox's writable
set, so no `allowWrite` widening is needed — this entry costs
nothing in sandbox surface.
- `<path-slug>` matches the convention Claude Code already uses for
its own scratch tree, so the directory sits alongside the
session's existing state instead of introducing a second
location.
- Scoping to `settings.local.json` rather than user-scope
`settings.json` is what makes the value per-project. A
user-scope `TMPDIR` would be shared by every repo and would
reintroduce the collision mode.

Create the directory before first use — a `TMPDIR` naming a
non-existent path fails the same way.
Claude Code sets `TMPDIR` itself when it builds the sandbox, to the
shared session root `/tmp/claude-<uid>`, and that assignment wins
over the settings value. The override is specific to `TMPDIR`:
other `env` keys from the same file do take effect, so a session
can show a live `CONTAINER_HOST` from project settings and a
`TMPDIR` that ignores them. The symptom of having tried is a
directory that exists, is named exactly as configured, and stays
empty for the life of the setting.

In practice the collision risk this case describes is mostly
absorbed elsewhere: each session also gets its own scratchpad
under `/tmp/claude-<uid>/<path-slug>/<session-id>/`, which is
per-project and per-session by construction. Prefer that for
anything a skill or tool writes; treat a bare `$TMPDIR` as shared
with every other project on the machine, and make temp filenames
unique rather than assuming the directory is yours.

### Notes

- **`env` is applied at session start.** The change does not take
effect in the session that makes it; restart, then confirm with
the doctor skill's *project-scratch* probe.
- **`env` is applied at session start.** For the keys that are
honored, a change does not take effect in the session that makes
it; restart, then confirm with the doctor skill's
*project-scratch* probe. `TMPDIR` is not one of those keys — see
the Fix above.
- The scratch directory **cannot** be remapped onto literal `/tmp`
inside the sandbox. `sandbox.filesystem.*` accepts allow / deny
path lists only — there is no bind-mount or path-remap key.
Expand Down
39 changes: 17 additions & 22 deletions docs/setup/secure-agent-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,14 +462,13 @@ below, annotated.

```jsonc
{
// The container gateway (tools/container-gateway) is where sandboxed
// podman / docker calls go. Both CLIs honour these variables; the
// sockets are project-relative, so this block is the same for every
// adopter. The gateway is started by the SessionStart hook below.
"env": {
"CONTAINER_HOST": "unix://./.apache-magpie-local/run/podman.sock",
"DOCKER_HOST": "unix://./.apache-magpie-local/run/docker.sock"
},
// No `env` block here. Sandboxed podman / docker calls go through the
// container gateway (tools/container-gateway), but CONTAINER_HOST /
// DOCKER_HOST have to name its sockets by ABSOLUTE path — the CLIs do
// not resolve a project-relative `unix://./…` value against the cwd —
// and an absolute path is per-machine. So they live in the gitignored
// `.claude/settings.local.json` alongside the matching
// `allowUnixSockets` entries, not here. See "Container gateway" below.
"sandbox": {
"enabled": true,
// `excludedCommands` runs the listed commands OUTSIDE the sandbox.
Expand Down Expand Up @@ -2369,25 +2368,17 @@ Wire it as a `SessionStart` / `SessionEnd` pair in `~/.claude/settings.json`, al
}
```

The framework's own `.claude/settings.json` already carries the `env` half of the project-settings block, using project-relative `unix://` URLs so the same file works in every worktree:

```jsonc
// .claude/settings.json (committed, project-wide)
{
"env": {
"CONTAINER_HOST": "unix://./.apache-magpie-local/run/podman.sock",
"DOCKER_HOST": "unix://./.apache-magpie-local/run/docker.sock"
}
}
```

`allowUnixSockets` entries need an absolute path, which is per-machine, so they belong in the gitignored `.claude/settings.local.json` instead.
Add the block by hand, substituting your own project's absolute path for `<project>` — nothing writes it for you.
Every setting that points something at a gateway socket needs that socket's **absolute** path, which is per-machine, so the whole project-settings block belongs in the gitignored `.claude/settings.local.json` — nothing is committed.
Add it by hand, substituting your own project's absolute path for `<project>` — nothing writes it for you.
(`setup-isolated-setup-install` Step L proposes the same block as a settings diff; `/magpie-setup config` does **not** write it, and automating it there is a recorded follow-up.)

```jsonc
// .claude/settings.local.json (gitignored, per machine)
{
"env": {
"CONTAINER_HOST": "unix:///<project>/.apache-magpie-local/run/podman.sock",
"DOCKER_HOST": "unix:///<project>/.apache-magpie-local/run/docker.sock"
},
"sandbox": {
"network": {
"allowUnixSockets": [
Expand All @@ -2399,6 +2390,10 @@ Add the block by hand, substituting your own project's absolute path for `<proje
}
```

**Do not use a project-relative `unix://./…` value**, even though it would be worktree-portable and this guide recommended it until recently.
The CLIs do not resolve it against the cwd: a `unix://` URL's authority is parsed as a host component, so `unix://./.apache-magpie-local/run/podman.sock` dials `/.//.apache-magpie-local/run/podman.sock` and `unix://.apache-magpie-local/run/podman.sock` dials `/.apache-magpie-local//run/podman.sock`, neither of which exists (verified against podman 6.1.0).
`unix:///absolute/path` is the only spelling that reaches the socket, and paying for it in a per-machine file is the cost of that.

Never add the real daemon socket to `allowUnixSockets` under any name: the framework's `sandbox-lint` tool rejects an entry whose basename is `docker.sock`, `podman.sock`, or ends in `-api.sock`, unless its parent directory is `.apache-magpie-local/run`.

### Egress
Expand Down
43 changes: 31 additions & 12 deletions plugins/magpie-setup/skills/isolated-setup-doctor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,17 @@ for rt in podman docker; do
echo "PROBE: ${rt}-runtime → ✗ ($( [ "$rt" = podman ] && echo CONTAINER_HOST || echo DOCKER_HOST ) unset — gateway not wired into settings)"
continue
fi
# The CLIs read a unix:// URL's authority as a host component, so only the
# absolute "unix:///path" spelling reaches the socket: "unix://./x" dials
# "/.//x". A relative value still stats fine here (the socket does exist
# relative to the cwd), so catch it by shape or the real cause is lost in
# the generic failure branch below.
case "$url" in
unix:///*) ;;
unix://*|unix:*)
echo "PROBE: ${rt}-runtime → ✗ ($( [ "$rt" = podman ] && echo CONTAINER_HOST || echo DOCKER_HOST )=$url is not absolute — the CLIs do not resolve a relative unix:// value against the cwd; use unix:///<project>/.apache-magpie-local/run/$rt.sock)"
continue ;;
esac
sock="${url#unix://}"
case "$(gw_state "$rt")" in
not-running)
Expand Down Expand Up @@ -367,12 +378,17 @@ per the catalog entry below.

### Probe 4 — Per-project scratch directory (`TMPDIR`)

Tests whether the session has a **writable, per-project** scratch
directory. The sandbox mounts the host `/tmp` read-only and punches
only specific subpaths writable, so a session whose `TMPDIR` falls
back to `/tmp` gets no scratch area at all, and one whose `TMPDIR`
points at the shared session root gets an area that collides with
every other project on the machine.
Tests whether the session has a **writable** scratch directory. The
sandbox mounts the host `/tmp` read-only and punches only specific
subpaths writable, so a session whose `TMPDIR` falls back to `/tmp`
gets no scratch area at all.

`TMPDIR` landing on the shared session root rather than a
per-project directory is **not** a finding. Claude Code sets
`TMPDIR` itself when it builds the sandbox and that assignment wins
over `env.TMPDIR` from any settings file, so the shared root is the
expected value and no configuration changes it. Each session still
gets a per-project, per-session scratchpad underneath it.

**Command:**

Expand All @@ -388,7 +404,7 @@ else
slug=$(pwd | sed 's|/|-|g')
case "$TMPDIR" in
*"$slug"*) echo "PROBE: project-scratch → ✓ (per-project + writable: $TMPDIR)" ;;
*) echo "PROBE: project-scratch → (writable but shared across projects: $TMPDIR)" ;;
*) echo "PROBE: project-scratch → (writable; shared session root, which is the harness default: $TMPDIR)" ;;
esac
fi
```
Expand All @@ -398,16 +414,19 @@ fi
| Result | Status | Meaning |
|---|---|---|
| `✓ per-project + writable` | Pass | `TMPDIR` resolves under this project's path slug and accepts writes. |
| ` writable but shared across projects` | Warn | Scratch works, but every project on this machine shares it; concurrent sessions can collide on identical temp filenames. |
| ` writable; shared session root` | Pass | The expected value on current Claude Code. Every project on the machine shares this directory, so write through the per-session scratchpad beneath it, or use unique filenames — but there is nothing to fix. |
| `✗ TMPDIR not set` | Fail | Tooling falls back to `/tmp`, which is read-only inside the sandbox. |
| `✗ directory missing` | Fail | `env.TMPDIR` names a path nothing has created yet. |
| `✗ directory missing` | Fail | `TMPDIR` names a path nothing has created yet. |
| `✗ not writable inside sandbox` | Fail | `TMPDIR` points outside `sandbox.filesystem.allowWrite`. |

**On ✗ / ⚠ → remediation:**
**On ✗ → remediation:**
[`docs/setup/sandbox-troubleshooting.md` — Temp files fail with "Read-only file system" under `/tmp`](../../../../docs/setup/sandbox-troubleshooting.md#temp-files-fail-with-read-only-file-system-under-tmp).

Note that `env` is applied at session start, so a fix does not take
effect in the session that makes it — restart before re-probing.
Do **not** propose `env.TMPDIR` in a settings file as the fix.
Claude Code overrides it when it builds the sandbox, so the setting
is accepted and silently has no effect; the giveaway is a directory
that exists, is named exactly as configured, and stays empty. The
catalog entry above covers what is actually actionable.

### Probe 5 — Signing key readable (`gpg.format=ssh`)

Expand Down
16 changes: 10 additions & 6 deletions plugins/magpie-setup/skills/isolated-setup-install/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,12 +735,16 @@ populated needs neither, since the hook falls back to
merging into existing arrays with a diff the operator approves,
exactly as for K.2's touch-overlay hooks.

**L.3 — Project wiring.** Propose the project `env` block
(`CONTAINER_HOST` / `DOCKER_HOST`, project-relative `unix://` URLs,
committed in `.claude/settings.json`) and the `allowUnixSockets`
pair (absolute paths, per-machine, in the gitignored
`.claude/settings.local.json`) as a single settings diff — the same
two-file split the setup guide documents. Never propose the real
**L.3 — Project wiring.** Propose the `env` block
(`CONTAINER_HOST` / `DOCKER_HOST`) and the `allowUnixSockets` pair
as a single settings diff into the gitignored
`.claude/settings.local.json`. All four values are **absolute**
paths and therefore per-machine: the CLIs do not resolve a
project-relative `unix://./…` value against the cwd — the URL
authority is read as a host component, so `unix://./x` dials
`/.//x` — and `allowUnixSockets` has no relative form either.
Nothing gateway-related is committed to `.claude/settings.json`.
Never propose the real
daemon socket under any name; `tools/sandbox-lint` rejects an
`allowUnixSockets` entry named `docker.sock` / `podman.sock` /
`*-api.sock` outside `.apache-magpie-local/run/`.
Expand Down
Loading
Loading