From 3e45bc2c717f75edc5573b223bbaf4945f75c763 Mon Sep 17 00:00:00 2001 From: Kristin Kim Date: Fri, 8 May 2026 15:02:28 -0700 Subject: [PATCH 1/2] fix(sandbox): don't denyWrite read-only mounts so nested rw subdirs work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @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/) is mounted read-write inside it. The agent crashed on first run with `EPERM: mkdir 'groups//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. --- src/runtimes/sandbox-runner.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/runtimes/sandbox-runner.ts b/src/runtimes/sandbox-runner.ts index 1c880df..f0011f2 100644 --- a/src/runtimes/sandbox-runner.ts +++ b/src/runtimes/sandbox-runner.ts @@ -300,7 +300,10 @@ export function buildSandboxSettings( denyWrite.push(mount.hostPath); } else if (mount.readonly) { allowRead.push(mount.hostPath); - denyWrite.push(mount.hostPath); // enforce read-only at srt level + // Do NOT push to denyWrite: per srt config, denyWrite takes precedence + // over allowWrite, so adding a parent path here blocks nested writable + // mounts (e.g. groupDir inside projectRoot). Read-only is enforced + // implicitly: paths not present in allowWrite cannot be written. } else { // read-write allowWrite.push(mount.hostPath); From 375cbbc6c90534d9b315df1c3e4edfa04a8d9f6a Mon Sep 17 00:00:00 2001 From: Kristin Kim Date: Fri, 8 May 2026 15:02:40 -0700 Subject: [PATCH 2/2] fix(setup): use STATE_ROOT for service label detection in verify 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 /data, the two scripts computed different labels: setup/service.ts: com.claudeclaw. (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 (= /data) so verify and service agree on the label. --- setup/verify.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/verify.ts b/setup/verify.ts index 1238500..f57cb84 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -11,7 +11,7 @@ import path from 'path'; import Database from 'better-sqlite3'; -import { DATA_DIR, STORE_DIR } from '../src/orchestrator/config.js'; +import { DATA_DIR, STATE_ROOT, STORE_DIR } from '../src/orchestrator/config.js'; import { readEnvFile } from '../src/orchestrator/env.js'; import { logger } from '../src/orchestrator/logger.js'; import { @@ -23,7 +23,7 @@ import { import { emitStatus } from './status.js'; // Derive service label from data directory for instance-specific checks -const SERVICE_DIR_NAME = path.basename(DATA_DIR).replace(/[^a-zA-Z0-9_-]/g, '-'); +const SERVICE_DIR_NAME = path.basename(STATE_ROOT).replace(/[^a-zA-Z0-9_-]/g, '-'); const SERVICE_LABEL = `com.claudeclaw.${SERVICE_DIR_NAME}`; const SYSTEMD_UNIT = `claudeclaw-${SERVICE_DIR_NAME}`;