From 47cd5a2db4f5737be011a1ae26a240235e59007a Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Sat, 3 Oct 2026 15:22:18 -0400 Subject: [PATCH 1/5] fix(runner): keep headless Grok read-only lanes from cancelling (MOB-1466) Grok's plan permission mode prompts for any shell command outside its built-in read-only list, and a headless run cancels the whole turn on a prompt, ending the lane as malformed-output. Read-only Grok lanes now use auto mode, which reports a blocked call to the model; the read-only sandbox still blocks writes. Malformed-output evidence now keeps the tail of the stream, where the provider's terminal event is. Co-Authored-By: Claude Opus 5.5 --- .../references/provider-dispatch.md | 2 +- .../scripts/runner/commands.test.ts | 2 +- .../poteto-mode/scripts/runner/commands.ts | 8 ++++++- .../poteto-mode/scripts/runner/run.test.ts | 21 +++++++++++++++++++ .../skills/poteto-mode/scripts/runner/run.ts | 7 ++++++- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index e0d62a2a..5f79cc4c 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -80,7 +80,7 @@ Start the background process, continue launching the other lanes, then drain the The runner and its preflight have no implicit timeout. Do not invent a duration from role, mode, or a convenient round number; real implementation lanes can run for 90 minutes or much longer. Pass `--timeout` only when the user, an external service deadline, or a measured task contract supplies a real bound. That value starts at wrapper entry, before module loading and argument parsing, and remains one absolute deadline across setup, preflight, model execution, and output capture. It is never a fresh allowance per child, and long waits are armed in runtime-safe chunks without shortening the supplied deadline. Otherwise supervise liveness through the retained background task/session handle and cancel manually only on evidence that the run is dead. Cancel through that retained handle so the runner receives SIGINT or SIGTERM, sends it to an active child when one remains, stops waiting on inherited output pipes, removes the empty output reservation, and writes a `cancelled` receipt. Preserve that receipt; a retry is a new attempt with new unique output and receipt paths. Unchanged running state is not a dropout, and Claude's ten-minute foreground ceiling is never a reason to terminate a healthy lane. -Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok plan mode plus its `read-only` sandbox and read-oriented tool list. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, and Grok `acceptEdits` plus its `workspace` sandbox and write-capable tool list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. +Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, and Grok `acceptEdits` plus its `workspace` sandbox and write-capable tool list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. Every concurrent external lane needs distinct prompt, output, and receipt paths. The launcher reserves output and receipt paths exclusively and refuses to overwrite them. diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts index ea697ff2..c88973d9 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts @@ -96,7 +96,7 @@ describe("invocationCommand", () => { "--reasoning-effort", "xhigh", "--permission-mode", - "plan", + "auto", "--sandbox", "read-only", "--tools", diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts index 5f2b10c6..7c368522 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts @@ -59,6 +59,12 @@ function permissionMode(mode: AccessMode): string { return mode === "read-only" ? "plan" : "acceptEdits"; } +// Headless Grok cancels the whole turn on a permission prompt. Its auto mode reports a +// blocked call to the model instead, and the read-only sandbox still blocks writes. +function grokPermissionMode(mode: AccessMode): string { + return mode === "read-only" ? "auto" : "acceptEdits"; +} + function effortOverride(effort: Effort): string { return `model_reasoning_effort=${JSON.stringify(effort)}`; } @@ -129,7 +135,7 @@ export function invocationCommand(options: RunnerOptions): CommandSpec { "--reasoning-effort", options.effort, "--permission-mode", - permissionMode(options.mode), + grokPermissionMode(options.mode), "--sandbox", grokSandbox(options.mode), "--tools", diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts index 20743b52..c114d102 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts @@ -120,6 +120,9 @@ if (name === "claude") { console.log(JSON.stringify({type:"thread.started",thread_id:"o1"})); console.log(JSON.stringify({type:"item.completed",item:{type:"agent_message",text:"CODEX_OK"}})); console.log(JSON.stringify({type:"turn.completed",usage:{input_tokens:20,cached_input_tokens:5,output_tokens:3,reasoning_output_tokens:1}})); +} else if (process.env.FAKE_GROK_ERROR_RESULT === "1") { + console.log(JSON.stringify({type:"system",subtype:"init",skills:Array(500).fill("skill-name")})); + console.log(JSON.stringify({type:"result",subtype:"error_during_execution",is_error:true,stop_reason:"cancelled",errors:["cancelled"]})); } else { console.log(JSON.stringify({type:"assistant",message:{content:[{type:"text",text:"progress"}]}})); console.log(JSON.stringify({type:"result",subtype:"success",is_error:false,result:"GROK_OK",session_id:"g1",usage:{input_tokens:30,output_tokens:4,total_tokens:34},total_cost_usd:0.02,modelUsage:{[model + "-build"]:{}}})); @@ -244,6 +247,7 @@ beforeEach(() => { delete process.env.FAKE_DESCENDANT_HOLDS_PIPES_MS; delete process.env.FAKE_DESCENDANT_PID_PATH; delete process.env.FAKE_SELF_SIGNAL; + delete process.env.FAKE_GROK_ERROR_RESULT; }); afterEach(() => { @@ -268,6 +272,7 @@ afterEach(() => { delete process.env.FAKE_DESCENDANT_HOLDS_PIPES_MS; delete process.env.FAKE_DESCENDANT_PID_PATH; delete process.env.FAKE_SELF_SIGNAL; + delete process.env.FAKE_GROK_ERROR_RESULT; rmSync(scratch, { recursive: true, force: true }); }); @@ -307,6 +312,22 @@ describe("runLane", () => { }); }); + it("keeps Grok's terminal error result in malformed-output evidence", async () => { + process.env.FAKE_GROK_ERROR_RESULT = "1"; + const input = options("grok", "grok-error-result"); + const result = await runLane(input); + expect(result.exitCode).toBe(65); + expect(existsSync(input.outputPath)).toBe(false); + const recorded = receipt(input.receiptPath); + expect(recorded).toMatchObject({ + status: "malformed-output", + exitCode: 0, + error: { message: "grok reported an error result" }, + }); + expect(recorded.error?.evidence).toContain('"stop_reason":"cancelled","errors":["cancelled"]'); + expect(recorded.error?.evidence.length).toBeLessThanOrEqual(4_000); + }); + it("classifies an unavailable model without falling back", async () => { process.env.FAKE_INVALID_MODEL = "1"; const input = options("codex"); diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts index 054564a4..aa887f00 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts @@ -52,6 +52,11 @@ function evidence(value: string): string { return value.trim().slice(0, ERROR_EVIDENCE_LIMIT); } +// A provider's terminal event ends its stream, so malformed output keeps the tail. +function trailingEvidence(value: string): string { + return value.trim().slice(-ERROR_EVIDENCE_LIMIT); +} + function removeIfExists(path: string): void { if (existsSync(path)) unlinkSync(path); } @@ -839,7 +844,7 @@ async function executeLane( costUsd: null, error: { message, - evidence: evidence(`${result.stderr}\n${result.stdout}`), + evidence: trailingEvidence(`${result.stderr}\n${result.stdout}`), }, }); } From fb2c4f8828e3a621a15df7f73eb7d191c175e22d Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Sat, 3 Oct 2026 16:43:21 -0400 Subject: [PATCH 2/5] fix(runner): run Grok write lanes in auto permission mode (MOB-1466) Headless Grok 1.0.46 cancels the whole turn when acceptEdits raises a permission prompt, which it does for any shell command outside Grok's built-in list. The earlier MOB-1466 investigation reproduced this with the workspace sandbox and a `find` prompt: the turn ended cancelled. This is the same defect that 47cd5a2 fixed for read-only plan mode. Grok now passes --permission-mode auto for both access modes. Auto mode reports a blocked call to the model instead of cancelling, and the sandbox still confines writes (read-only, or workspace for isolated-write). Claude and Codex modes are unchanged. Verified with the runner's isolated-write argv in a throwaway worktree: `find . -maxdepth 1 -name README.md` ran (exit 0), MOB1466.txt was written inside the worktree, and the result event was subtype success, stop_reason end_turn. Co-Authored-By: Claude Opus 5.5 --- .../poteto-mode/references/provider-dispatch.md | 2 +- .../poteto-mode/scripts/runner/commands.test.ts | 2 +- .../skills/poteto-mode/scripts/runner/commands.ts | 11 ++++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 5f79cc4c..005c5620 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -80,7 +80,7 @@ Start the background process, continue launching the other lanes, then drain the The runner and its preflight have no implicit timeout. Do not invent a duration from role, mode, or a convenient round number; real implementation lanes can run for 90 minutes or much longer. Pass `--timeout` only when the user, an external service deadline, or a measured task contract supplies a real bound. That value starts at wrapper entry, before module loading and argument parsing, and remains one absolute deadline across setup, preflight, model execution, and output capture. It is never a fresh allowance per child, and long waits are armed in runtime-safe chunks without shortening the supplied deadline. Otherwise supervise liveness through the retained background task/session handle and cancel manually only on evidence that the run is dead. Cancel through that retained handle so the runner receives SIGINT or SIGTERM, sends it to an active child when one remains, stops waiting on inherited output pipes, removes the empty output reservation, and writes a `cancelled` receipt. Preserve that receipt; a retry is a new attempt with new unique output and receipt paths. Unchanged running state is not a dropout, and Claude's ten-minute foreground ceiling is never a reason to terminate a healthy lane. -Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, and Grok `acceptEdits` plus its `workspace` sandbox and write-capable tool list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. +Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, and Grok auto mode plus its `workspace` sandbox and write-capable tool list. Grok uses auto mode in both access modes because `acceptEdits` raises the same turn-cancelling prompt for a shell command outside Grok's built-in list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. Every concurrent external lane needs distinct prompt, output, and receipt paths. The launcher reserves output and receipt paths exclusively and refuses to overwrite them. diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts index c88973d9..a1574538 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts @@ -124,7 +124,7 @@ describe("invocationCommand", () => { expect(grok.args).toEqual( expect.arrayContaining([ "--permission-mode", - "acceptEdits", + "auto", "--sandbox", "workspace", "--tools", diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts index 7c368522..6edb4693 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts @@ -59,12 +59,6 @@ function permissionMode(mode: AccessMode): string { return mode === "read-only" ? "plan" : "acceptEdits"; } -// Headless Grok cancels the whole turn on a permission prompt. Its auto mode reports a -// blocked call to the model instead, and the read-only sandbox still blocks writes. -function grokPermissionMode(mode: AccessMode): string { - return mode === "read-only" ? "auto" : "acceptEdits"; -} - function effortOverride(effort: Effort): string { return `model_reasoning_effort=${JSON.stringify(effort)}`; } @@ -134,8 +128,11 @@ export function invocationCommand(options: RunnerOptions): CommandSpec { options.model, "--reasoning-effort", options.effort, + // Headless Grok cancels the whole turn on a permission prompt, in both access modes. + // Auto mode reports a blocked call to the model instead; the sandbox still confines + // writes (read-only, or workspace for isolated-write). "--permission-mode", - grokPermissionMode(options.mode), + "auto", "--sandbox", grokSandbox(options.mode), "--tools", From 2a731d2bf6a9deed851db5b01032d436132772e0 Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Sat, 3 Oct 2026 17:44:41 -0400 Subject: [PATCH 3/5] fix(runner): run Claude write lanes in auto permission mode The Claude isolated-write lane now passes --permission-mode auto, matching the Grok lane. The read-only lane stays plan because the runner gives the Claude lane no OS sandbox, so auto mode there could let Bash write into the primary checkout. Evidence, Claude Code 2.1.288, runner isolated-write argv, throwaway worktree: - Before (acceptEdits, haiku): `find . -maxdepth 1 -name README.md` ran and Write created PARITY.txt. A Bash interpreter write (`python3 -c "open('PY.txt','w')..."`) was denied: "The following part requires approval", one permission_denials entry. The turn was not cancelled. - After (auto, sonnet and opus): init reports permissionMode auto; find ran, Write created PARITY.txt, and the python3 write ran with no denials. PY.txt landed in the worktree only, not in the primary checkout. - After (auto, haiku): init reports permissionMode default; Haiku 4.5 has no auto-mode support, so Write was denied. Documented in provider-dispatch.md. Co-Authored-By: Claude Opus 5.5 --- .../skills/poteto-mode/references/provider-dispatch.md | 2 +- .../skills/poteto-mode/scripts/runner/commands.test.ts | 2 +- .../pstack/skills/poteto-mode/scripts/runner/commands.ts | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 005c5620..1deaa4ec 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -80,7 +80,7 @@ Start the background process, continue launching the other lanes, then drain the The runner and its preflight have no implicit timeout. Do not invent a duration from role, mode, or a convenient round number; real implementation lanes can run for 90 minutes or much longer. Pass `--timeout` only when the user, an external service deadline, or a measured task contract supplies a real bound. That value starts at wrapper entry, before module loading and argument parsing, and remains one absolute deadline across setup, preflight, model execution, and output capture. It is never a fresh allowance per child, and long waits are armed in runtime-safe chunks without shortening the supplied deadline. Otherwise supervise liveness through the retained background task/session handle and cancel manually only on evidence that the run is dead. Cancel through that retained handle so the runner receives SIGINT or SIGTERM, sends it to an active child when one remains, stops waiting on inherited output pipes, removes the empty output reservation, and writes a `cancelled` receipt. Preserve that receipt; a retry is a new attempt with new unique output and receipt paths. Unchanged running state is not a dropout, and Claude's ten-minute foreground ceiling is never a reason to terminate a healthy lane. -Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, and Grok auto mode plus its `workspace` sandbox and write-capable tool list. Grok uses auto mode in both access modes because `acceptEdits` raises the same turn-cancelling prompt for a shell command outside Grok's built-in list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. +Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list (not auto mode, because the runner gives Claude no OS sandbox to stop Bash writes), Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude auto mode with project-only settings, Codex `workspace-write`, and Grok auto mode plus its `workspace` sandbox and write-capable tool list. Claude Code 2.1.288 starts a model without auto-mode support, such as Haiku 4.5, in `default` mode instead, which denies Write and Edit headlessly. Grok uses auto mode in both access modes because `acceptEdits` raises the same turn-cancelling prompt for a shell command outside Grok's built-in list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. Every concurrent external lane needs distinct prompt, output, and receipt paths. The launcher reserves output and receipt paths exclusively and refuses to overwrite them. diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts index a1574538..6557e8eb 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts @@ -139,7 +139,7 @@ describe("invocationCommand", () => { expect(claude.args).toEqual( expect.arrayContaining([ "--permission-mode", - "acceptEdits", + "auto", "--tools", "Read,Write,Edit,Grep,Glob,Bash", ]) diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts index 6edb4693..3576521b 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts @@ -55,10 +55,6 @@ function grokTools(mode: AccessMode): string { return [...readonly, ...(mode === "isolated-write" ? ["search_replace"] : [])].join(","); } -function permissionMode(mode: AccessMode): string { - return mode === "read-only" ? "plan" : "acceptEdits"; -} - function effortOverride(effort: Effort): string { return `model_reasoning_effort=${JSON.stringify(effort)}`; } @@ -75,7 +71,8 @@ export function invocationCommand(options: RunnerOptions): CommandSpec { "--effort", options.effort, "--permission-mode", - permissionMode(options.mode), + // The Claude lane has no OS sandbox, so auto mode on read-only could let Bash write. + options.mode === "read-only" ? "plan" : "auto", "--setting-sources", "project", "--strict-mcp-config", From 469ea9e659ffafaaa3f9fb7510e54dc9582bf161 Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Sat, 3 Oct 2026 18:54:16 -0400 Subject: [PATCH 4/5] fix(runner): confine Claude write lanes with the Bash sandbox Unfret (request_changes) found that 2a731d2 broke the isolated-write contract: under --permission-mode auto the runner gives the Claude lane no OS sandbox, so a classifier is the only thing between Bash and the rest of the disk. Haiku 4.5 also starts in default mode under auto and is denied Write. The Claude isolated-write lane now runs acceptEdits plus --settings '{"sandbox":{"enabled":true,"autoAllowBashIfSandboxed":true, "allowUnsandboxedCommands":false,"failIfUnavailable":true}}'. Sandboxed Bash runs without prompts and can write only to the cwd and the per-user temp directory (Seatbelt on macOS). allowUnsandboxedCommands=false makes Claude Code ignore the dangerouslyDisableSandbox parameter and makes the sandbox admin-required, so repository settings cannot add excludedCommands or write paths. The read-only lane stays on plan. Evidence, Claude Code 2.1.288, runner isolated-write argv from invocationCommand, effort low, throwaway worktree under ~/projects: - Before (auto, sonnet): find, Write and the python3 write ran, and `echo probe > ~/projects/escape-probe-OUTSIDE.txt` and `touch ../escape-sibling.txt` both succeeded with 0 denials. The dangerouslyDisableSandbox retry also wrote. curl https://example.com returned HTTP/2 200. - After (acceptEdits + sandbox), opus, sonnet and claude-haiku-4-5-20251001: find ran, Write created WRITE.txt and python3 created PY.txt with 0 permission denials. Both outside writes failed with "Operation not permitted" and neither file existed afterwards. Opus retried the outside write with dangerouslyDisableSandbox=true and it still failed. curl got a 403 blocked-by-allowlist from the sandbox proxy; the turn continued. - After, sonnet, with a worktree .claude/settings.json that allows Bash(touch *), excludes `touch *` from the sandbox and adds ~/projects to allowWrite: `touch ../escape-sibling.txt` still failed, and Write to a path outside the cwd was a permission denial. Co-Authored-By: Claude Opus 5.5 --- .../references/provider-dispatch.md | 2 +- .../scripts/runner/commands.test.ts | 11 +++++++++- .../poteto-mode/scripts/runner/commands.ts | 22 ++++++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 1deaa4ec..873972bf 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -80,7 +80,7 @@ Start the background process, continue launching the other lanes, then drain the The runner and its preflight have no implicit timeout. Do not invent a duration from role, mode, or a convenient round number; real implementation lanes can run for 90 minutes or much longer. Pass `--timeout` only when the user, an external service deadline, or a measured task contract supplies a real bound. That value starts at wrapper entry, before module loading and argument parsing, and remains one absolute deadline across setup, preflight, model execution, and output capture. It is never a fresh allowance per child, and long waits are armed in runtime-safe chunks without shortening the supplied deadline. Otherwise supervise liveness through the retained background task/session handle and cancel manually only on evidence that the run is dead. Cancel through that retained handle so the runner receives SIGINT or SIGTERM, sends it to an active child when one remains, stops waiting on inherited output pipes, removes the empty output reservation, and writes a `cancelled` receipt. Preserve that receipt; a retry is a new attempt with new unique output and receipt paths. Unchanged running state is not a dropout, and Claude's ten-minute foreground ceiling is never a reason to terminate a healthy lane. -Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list (not auto mode, because the runner gives Claude no OS sandbox to stop Bash writes), Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude auto mode with project-only settings, Codex `workspace-write`, and Grok auto mode plus its `workspace` sandbox and write-capable tool list. Claude Code 2.1.288 starts a model without auto-mode support, such as Haiku 4.5, in `default` mode instead, which denies Write and Edit headlessly. Grok uses auto mode in both access modes because `acceptEdits` raises the same turn-cancelling prompt for a shell command outside Grok's built-in list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. +Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings plus the Bash sandbox, Codex `workspace-write`, and Grok auto mode plus its `workspace` sandbox and write-capable tool list. The Claude writer passes `--settings` with `sandbox.enabled`, `autoAllowBashIfSandboxed`, `allowUnsandboxedCommands: false` and `failIfUnavailable`. Bash and its child processes then run without a permission prompt under the operating system sandbox (Seatbelt on macOS, bubblewrap on Linux and WSL2). That sandbox lets them write only to the cwd, the per-user temp directory and, for a linked worktree, the main repository's shared `.git`. The model cannot take a command out of the sandbox: Claude Code ignores the Bash `dangerouslyDisableSandbox` parameter, and repository settings cannot add `excludedCommands` or write paths. If the sandbox cannot start, Claude Code exits instead of running Bash unsandboxed. Sandboxed network access goes through a proxy whose allowlist starts empty, so headless writers cannot reach any host: `curl` gets a 403 `blocked-by-allowlist`. The sandbox does not cover the Write and Edit tools, which `acceptEdits` approves only inside the working directory. Grok uses auto mode in both access modes because `acceptEdits` raises the same turn-cancelling prompt for a shell command outside Grok's built-in list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. Every concurrent external lane needs distinct prompt, output, and receipt paths. The launcher reserves output and receipt paths exclusively and refuses to overwrite them. diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts index 6557e8eb..37c1ba2a 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts @@ -139,11 +139,20 @@ describe("invocationCommand", () => { expect(claude.args).toEqual( expect.arrayContaining([ "--permission-mode", - "auto", + "acceptEdits", "--tools", "Read,Write,Edit,Grep,Glob,Bash", ]) ); + const settings = claude.args[claude.args.indexOf("--settings") + 1]; + expect(JSON.parse(settings ?? "")).toEqual({ + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + allowUnsandboxedCommands: false, + failIfUnavailable: true, + }, + }); }); it("covers low, medium, and high for every external provider", () => { diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts index 3576521b..a94114f3 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts @@ -42,6 +42,24 @@ function claudeTools(mode: AccessMode): string { : "Read,Write,Edit,Grep,Glob,Bash"; } +// Writers run Bash without prompts inside the OS sandbox, which confines writes to the cwd and a +// per-user temp directory. The model cannot opt a command out, and Claude refuses to start when +// the sandbox is unavailable. +const CLAUDE_WRITE_SANDBOX = JSON.stringify({ + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + allowUnsandboxedCommands: false, + failIfUnavailable: true, + }, +}); + +function claudePermissions(mode: AccessMode): string[] { + return mode === "read-only" + ? ["--permission-mode", "plan"] + : ["--permission-mode", "acceptEdits", "--settings", CLAUDE_WRITE_SANDBOX]; +} + function codexSandbox(mode: AccessMode): string { return mode === "read-only" ? "read-only" : "workspace-write"; } @@ -70,9 +88,7 @@ export function invocationCommand(options: RunnerOptions): CommandSpec { options.model, "--effort", options.effort, - "--permission-mode", - // The Claude lane has no OS sandbox, so auto mode on read-only could let Bash write. - options.mode === "read-only" ? "plan" : "auto", + ...claudePermissions(options.mode), "--setting-sources", "project", "--strict-mcp-config", From cb0b4797bbf720d98f9c6d732672bd4110a5962e Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Sat, 3 Oct 2026 19:35:29 -0400 Subject: [PATCH 5/5] revert(runner): move Claude write-lane changes to a separate PR (MOB-1466) Restore the Claude isolated-write lane to acceptEdits without the Bash sandbox, as on main. Review of this PR found two isolation gaps in the Claude writer changes. The first was auto mode writing outside the worktree. The second was Claude Code's sandbox permitting writes to a linked worktree's shared .git. Grok's workspace sandbox refuses those writes ("cannot lock ref"). This PR keeps the Grok fix. The Claude writer work continues on branch claude/claude-writer-sandbox, so it can be designed with the shared .git excluded. Co-Authored-By: Claude Opus 5.5 --- .../references/provider-dispatch.md | 2 +- .../scripts/runner/commands.test.ts | 9 ------- .../poteto-mode/scripts/runner/commands.ts | 25 +++++-------------- 3 files changed, 7 insertions(+), 29 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 873972bf..005c5620 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -80,7 +80,7 @@ Start the background process, continue launching the other lanes, then drain the The runner and its preflight have no implicit timeout. Do not invent a duration from role, mode, or a convenient round number; real implementation lanes can run for 90 minutes or much longer. Pass `--timeout` only when the user, an external service deadline, or a measured task contract supplies a real bound. That value starts at wrapper entry, before module loading and argument parsing, and remains one absolute deadline across setup, preflight, model execution, and output capture. It is never a fresh allowance per child, and long waits are armed in runtime-safe chunks without shortening the supplied deadline. Otherwise supervise liveness through the retained background task/session handle and cancel manually only on evidence that the run is dead. Cancel through that retained handle so the runner receives SIGINT or SIGTERM, sends it to an active child when one remains, stops waiting on inherited output pipes, removes the empty output reservation, and writes a `cancelled` receipt. Preserve that receipt; a retry is a new attempt with new unique output and receipt paths. Unchanged running state is not a dropout, and Claude's ten-minute foreground ceiling is never a reason to terminate a healthy lane. -Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings plus the Bash sandbox, Codex `workspace-write`, and Grok auto mode plus its `workspace` sandbox and write-capable tool list. The Claude writer passes `--settings` with `sandbox.enabled`, `autoAllowBashIfSandboxed`, `allowUnsandboxedCommands: false` and `failIfUnavailable`. Bash and its child processes then run without a permission prompt under the operating system sandbox (Seatbelt on macOS, bubblewrap on Linux and WSL2). That sandbox lets them write only to the cwd, the per-user temp directory and, for a linked worktree, the main repository's shared `.git`. The model cannot take a command out of the sandbox: Claude Code ignores the Bash `dangerouslyDisableSandbox` parameter, and repository settings cannot add `excludedCommands` or write paths. If the sandbox cannot start, Claude Code exits instead of running Bash unsandboxed. Sandboxed network access goes through a proxy whose allowlist starts empty, so headless writers cannot reach any host: `curl` gets a 403 `blocked-by-allowlist`. The sandbox does not cover the Write and Edit tools, which `acceptEdits` approves only inside the working directory. Grok uses auto mode in both access modes because `acceptEdits` raises the same turn-cancelling prompt for a shell command outside Grok's built-in list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. +Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, and Grok auto mode plus its `workspace` sandbox and write-capable tool list. Grok uses auto mode in both access modes because `acceptEdits` raises the same turn-cancelling prompt for a shell command outside Grok's built-in list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout. Every concurrent external lane needs distinct prompt, output, and receipt paths. The launcher reserves output and receipt paths exclusively and refuses to overwrite them. diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts index 37c1ba2a..a1574538 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts @@ -144,15 +144,6 @@ describe("invocationCommand", () => { "Read,Write,Edit,Grep,Glob,Bash", ]) ); - const settings = claude.args[claude.args.indexOf("--settings") + 1]; - expect(JSON.parse(settings ?? "")).toEqual({ - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - allowUnsandboxedCommands: false, - failIfUnavailable: true, - }, - }); }); it("covers low, medium, and high for every external provider", () => { diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts index a94114f3..6edb4693 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts @@ -42,24 +42,6 @@ function claudeTools(mode: AccessMode): string { : "Read,Write,Edit,Grep,Glob,Bash"; } -// Writers run Bash without prompts inside the OS sandbox, which confines writes to the cwd and a -// per-user temp directory. The model cannot opt a command out, and Claude refuses to start when -// the sandbox is unavailable. -const CLAUDE_WRITE_SANDBOX = JSON.stringify({ - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - allowUnsandboxedCommands: false, - failIfUnavailable: true, - }, -}); - -function claudePermissions(mode: AccessMode): string[] { - return mode === "read-only" - ? ["--permission-mode", "plan"] - : ["--permission-mode", "acceptEdits", "--settings", CLAUDE_WRITE_SANDBOX]; -} - function codexSandbox(mode: AccessMode): string { return mode === "read-only" ? "read-only" : "workspace-write"; } @@ -73,6 +55,10 @@ function grokTools(mode: AccessMode): string { return [...readonly, ...(mode === "isolated-write" ? ["search_replace"] : [])].join(","); } +function permissionMode(mode: AccessMode): string { + return mode === "read-only" ? "plan" : "acceptEdits"; +} + function effortOverride(effort: Effort): string { return `model_reasoning_effort=${JSON.stringify(effort)}`; } @@ -88,7 +74,8 @@ export function invocationCommand(options: RunnerOptions): CommandSpec { options.model, "--effort", options.effort, - ...claudePermissions(options.mode), + "--permission-mode", + permissionMode(options.mode), "--setting-sources", "project", "--strict-mcp-config",