Skip to content

fix(sandbox): don't denyWrite read-only mounts; fix verify service label#21

Open
kskim9 wants to merge 2 commits into
sbusso:mainfrom
kskim9:fix/sandbox-readonly-and-verify-service-name
Open

fix(sandbox): don't denyWrite read-only mounts; fix verify service label#21
kskim9 wants to merge 2 commits into
sbusso:mainfrom
kskim9:fix/sandbox-readonly-and-verify-service-name

Conversation

@kskim9

@kskim9 kskim9 commented May 8, 2026

Copy link
Copy Markdown

Summary

Two bugs surfaced during a fresh setup with RUNTIME=sandbox on macOS (Seatbelt via @anthropic-ai/sandbox-runtime 0.0.42).

1. Sandbox: read-only mounts blocked nested writable subdirs (P0)

@anthropic-ai/sandbox-runtime's schema documents denyWrite as "taking precedence over allowWrite" (also visible in the installed copy at node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-config.js:109).

The previous code pushed every read-only mount to both allowRead and denyWrite, intending to "enforce read-only at srt level". But because denyWrite wins on conflict, this blocked writes to any subdirectory of a read-only mount — including the group's own workspace.

In the main-group default config:

  • projectRoot (e.g. ~/claudeclaw) is mounted read-only
  • groupDir (e.g. ~/claudeclaw/groups/slack_main) is mounted read-write inside it

The agent crashed on first run with:

EPERM: operation not permitted, mkdir '/Users/.../claudeclaw/groups/slack_main/memory'

…because denyWrite=projectRoot overrode allowWrite=groupDir. The message loop retried 5 times then gave up — silent fail from the user's perspective.

Fix: drop the denyWrite.push() for read-only mounts. The sandbox is allow-list-based; paths absent from allowWrite are already implicitly denied, which enforces read-only on the parent without blocking nested writable subdirs.

This bug means any user of the sandbox runtime with the default is_main=true group hits it the moment the agent tries to create its memory dir.

2. setup: verify and service disagreed on the service label

setup/service.ts derives the launchd/systemd label from basename(process.cwd()), but setup/verify.ts used basename(DATA_DIR). Since DATA_DIR resolves to <STATE_ROOT>/data, the two scripts computed different labels:

Script Label
setup/service.ts com.claudeclaw.<project-dirname>
setup/verify.ts com.claudeclaw.data (always)

Result: verify always reported SERVICE: not_found even when the service was running, producing STATUS: failed on the final setup step.

Fix: use STATE_ROOT instead of DATA_DIR in verify.ts so the two scripts agree.

Test plan

  • Fresh setup with RUNTIME=sandbox on macOS, single Slack DM as main group — confirmed EPERM reproduces on main and is gone after the patch
  • Captured the generated data/sandbox-settings/*.json to verify denyWrite no longer contains the project root
  • Confirmed agent successfully creates groups/<folder>/memory/, completes a query, and posts to Slack
  • npx tsx setup/index.ts --step verify now reports SERVICE: running / STATUS: success against a running launchd service
  • Container runtime regression test (untested — I only have sandbox configured locally; container path doesn't go through buildSandboxSettings so no behavior change is expected)

kskim9 added 2 commits May 8, 2026 15:02
@anthropic-ai/sandbox-runtime documents denyWrite as taking precedence
over allowWrite. The previous implementation pushed every read-only
mount to both allowRead and denyWrite, intending to "enforce read-only
at srt level". But because denyWrite wins on conflict, this blocked
writes to *any* subdirectory of a read-only mount — including the
group's own workspace.

In the main-group default config, projectRoot is mounted read-only and
groupDir (groups/<folder>) is mounted read-write inside it. The agent
crashed on first run with `EPERM: mkdir 'groups/<folder>/memory'`
because denyWrite=projectRoot overrode allowWrite=groupDir.

Fix: only push readonly mounts to allowRead, not denyWrite. The sandbox
is allow-list-based — paths absent from allowWrite are implicitly denied,
which already enforces read-only on the parent mount without blocking
nested writable subdirs.

Reproduced on macOS Seatbelt via @anthropic-ai/sandbox-runtime 0.0.42.
setup/service.ts derives the launchd/systemd service label from
basename(process.cwd()), but setup/verify.ts was using basename(DATA_DIR)
to look up the same service. Since DATA_DIR resolves to
<STATE_ROOT>/data, the two scripts computed different labels:

  setup/service.ts:  com.claudeclaw.<dirname>     (e.g. .claudeclaw)
  setup/verify.ts:   com.claudeclaw.data          (always)

Result: `verify` always reported SERVICE: not_found even when the
service was running, causing STATUS: failed on the final setup check.

Fix: import and use STATE_ROOT (= the project/data root) instead of
DATA_DIR (= <root>/data) so verify and service agree on the label.
Zimondata pushed a commit to Zimondata/claudeclaw that referenced this pull request Jun 5, 2026
CRITICAL sbusso#1: StopFailure hook replaced with Stop hook (agent/runner)
HIGH sbusso#9: Added 10s TTL cache for loadSenderAllowlist (perf hotspot)
HIGH sbusso#10: Async auth retry instead of execFileSync (non-blocking)
MEDIUM sbusso#16: Transactional deleteTask (crash safety)
MEDIUM sbusso#19: Added indexes on agent_runs table (query perf)
MEDIUM sbusso#20: Error handling for extension loading
MEDIUM sbusso#21: Real GroupQueue.shutdown() implementation

All changes compiled and tested. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant