From 4ee144692bc496ddacab1558fcfe0afcb63ddd26 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:15:58 +0530 Subject: [PATCH 1/2] fix(claude): close read-only writes through nested agents A read-only Claude subagent could modify the checkout: its own Edit was denied, but it started a nested agent whose Write succeeded. Read-only turns denied only Bash and Edit, and the sandbox's denyWrite covers Bash, not Claude's built-in file tools. Read-only turns now also deny Write, MultiEdit, NotebookEdit, and EnterWorktree, and every tool that starts another agent now or later (Agent, Task, Workflow, CronCreate, RemoteTrigger), since those agents do not inherit the parent's denies. Co-Authored-By: Claude Opus 5.5 --- src/local-agent-claude.test.ts | 7 +++++++ src/local-agent-claude.ts | 20 +++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/local-agent-claude.test.ts b/src/local-agent-claude.test.ts index eb1185cc2..b9ff7bf3e 100644 --- a/src/local-agent-claude.test.ts +++ b/src/local-agent-claude.test.ts @@ -273,6 +273,13 @@ test("authority settings map each write mode to permissions and sandbox", () => assert.ok((readOnlyPermissions.deny as string[]).includes("Bash")); // SAFETY: The surrounding checks or construction already match this type. assert.ok((readOnlyPermissions.deny as string[]).includes("Edit")); + + // A nested agent or another file tool must not reopen the writes the parent is denied. + for (const tool of ["Write", "MultiEdit", "NotebookEdit", "EnterWorktree", "Agent", "Task", "Workflow", "CronCreate", "RemoteTrigger"]) { + // SAFETY: The surrounding checks or construction already match this type. + assert.ok((readOnlyPermissions.deny as string[]).includes(tool), tool); + } + assert.deepEqual( // SAFETY: The surrounding checks or construction already match this type. ((readOnlySettings.sandbox as JsonObject).filesystem as JsonObject).allowWrite, diff --git a/src/local-agent-claude.ts b/src/local-agent-claude.ts index ac2ae6567..04aae26e2 100644 --- a/src/local-agent-claude.ts +++ b/src/local-agent-claude.ts @@ -23,6 +23,24 @@ import type { type ClaudePermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" | "auto"; +// Read-only turns deny every tool that writes locally (including EnterWorktree, which creates a Git worktree) and +// every tool that starts other agents now or later (Agent, Task, Workflow, CronCreate, RemoteTrigger): those agents +// do not inherit the parent's denies and could do the editing it is refused. The sandbox's denyWrite only covers +// Bash, not Claude's built-in file tools. +const CLAUDE_READ_ONLY_DENIED_TOOLS = [ + "Bash", + "Edit", + "Write", + "MultiEdit", + "NotebookEdit", + "EnterWorktree", + "Agent", + "Task", + "Workflow", + "CronCreate", + "RemoteTrigger", +] as const; + const CLAUDE_WORKSPACE_ALLOWED_TOOLS = [ // allowedTools is passed as a session/CLI rule, so `/` is anchored to the query cwd. "Read(/**)", @@ -386,7 +404,7 @@ function claudeAuthorityOptions( const permissions = { defaultMode: "dontAsk", - deny: allowed ? [] : ["Bash", "Edit"], + deny: allowed ? [] : [...CLAUDE_READ_ONLY_DENIED_TOOLS], }; const sandbox = { From edb06d63b7922cdeb0f551397f283d5e54ddab45 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:02:46 +0530 Subject: [PATCH 2/2] test(claude): record the full read-only deny list in replay transcripts Co-Authored-By: Claude Opus 5.5 --- test/agents-replay/transcripts/claude/read-only.jsonl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/agents-replay/transcripts/claude/read-only.jsonl b/test/agents-replay/transcripts/claude/read-only.jsonl index 3297f9e6d..1a4544f84 100644 --- a/test/agents-replay/transcripts/claude/read-only.jsonl +++ b/test/agents-replay/transcripts/claude/read-only.jsonl @@ -1,6 +1,6 @@ -{"type":"meta","provider":"claude","scenario":"read-only","args":["--output-format","stream-json","--verbose","--input-format","stream-json","--model","haiku","--allowedTools","Read(/**),Edit(/**),Bash","--permission-mode","dontAsk","--settings","{\"permissions\":{\"defaultMode\":\"dontAsk\",\"deny\":[\"Bash\",\"Edit\"]},\"sandbox\":{\"enabled\":true,\"failIfUnavailable\":true,\"autoAllowBashIfSandboxed\":true,\"allowUnsandboxedCommands\":false,\"filesystem\":{\"allowWrite\":[],\"denyWrite\":[\"$WORKSPACE\"]}}}"],"version":"2.1.200 (Claude Code)","recordedAt":"2026-10-26T07:10:31.902Z"} +{"type":"meta","provider":"claude","scenario":"read-only","args":["--output-format","stream-json","--verbose","--input-format","stream-json","--model","haiku","--allowedTools","Read(/**),Edit(/**),Bash","--permission-mode","dontAsk","--settings","{\"permissions\":{\"defaultMode\":\"dontAsk\",\"deny\":[\"Bash\",\"Edit\",\"Write\",\"MultiEdit\",\"NotebookEdit\",\"EnterWorktree\",\"Agent\",\"Task\",\"Workflow\",\"CronCreate\",\"RemoteTrigger\"]},\"sandbox\":{\"enabled\":true,\"failIfUnavailable\":true,\"autoAllowBashIfSandboxed\":true,\"allowUnsandboxedCommands\":false,\"filesystem\":{\"allowWrite\":[],\"denyWrite\":[\"$WORKSPACE\"]}}}"],"version":"2.1.200 (Claude Code)","recordedAt":"2026-10-26T07:10:31.902Z"} {"dir":"in","ms":113,"frame":{"request_id":"5w08fedkj87","type":"control_request","request":{"subtype":"initialize","systemPrompt":[""]}}} -{"dir":"in","ms":113,"frame":{"request_id":"5v6gd7b4kd6","type":"control_request","request":{"subtype":"apply_flag_settings","settings":{"permissions":{"defaultMode":"dontAsk","deny":["Bash","Edit"]},"sandbox":{"enabled":true,"failIfUnavailable":true,"autoAllowBashIfSandboxed":true,"allowUnsandboxedCommands":false,"filesystem":{"allowWrite":[],"denyWrite":["$WORKSPACE"]}}}}}} +{"dir":"in","ms":113,"frame":{"request_id":"5v6gd7b4kd6","type":"control_request","request":{"subtype":"apply_flag_settings","settings":{"permissions":{"defaultMode":"dontAsk","deny":["Bash","Edit","Write","MultiEdit","NotebookEdit","EnterWorktree","Agent","Task","Workflow","CronCreate","RemoteTrigger"]},"sandbox":{"enabled":true,"failIfUnavailable":true,"autoAllowBashIfSandboxed":true,"allowUnsandboxedCommands":false,"filesystem":{"allowWrite":[],"denyWrite":["$WORKSPACE"]}}}}}} {"dir":"out","ms":1154,"frame":{"type":"control_response","response":{"subtype":"success","request_id":"5w08fedkj87","response":{"commands":[],"agents":[],"output_style":"default","available_output_styles":["default","Proactive","Explanatory","Learning"],"models":[{"value":"default","resolvedModel":"claude-opus-4-8[1m]","displayName":"Default (recommended)","description":"Opus 4.8 with 1M context · Best for everyday, complex tasks","supportsEffort":true,"supportedEffortLevels":["low","medium","high","xhigh","max"],"supportsAdaptiveThinking":true,"supportsFastMode":true,"supportsAutoMode":true},{"value":"opus[1m]","resolvedModel":"claude-opus-4-8[1m]","displayName":"Opus","description":"Opus 4.8 with 1M context · Best for everyday, complex tasks","supportsEffort":true,"supportedEffortLevels":["low","medium","high","xhigh","max"],"supportsAdaptiveThinking":true,"supportsFastMode":true,"supportsAutoMode":true},{"value":"claude-fable-5[1m]","resolvedModel":"claude-fable-5","displayName":"Fable","description":"Fable 5 · Most capable for your hardest and longest-running tasks","supportsEffort":true,"supportedEffortLevels":["low","medium","high","xhigh","max"],"supportsAdaptiveThinking":true,"supportsAutoMode":true},{"value":"sonnet","resolvedModel":"claude-sonnet-5","displayName":"Sonnet","description":"Sonnet 5 · Efficient for routine tasks","supportsEffort":true,"supportedEffortLevels":["low","medium","high","xhigh","max"],"supportsAdaptiveThinking":true,"supportsAutoMode":true},{"value":"haiku","resolvedModel":"claude-haiku-4-5-20251001","displayName":"Haiku","description":"Haiku 4.5 · Fastest for quick answers"}],"account":{},"pid":0,"feedback_survey_config":{"minTimeBeforeFeedbackMs":600000,"minTimeBetweenFeedbackMs":43200000,"minTimeBetweenGlobalFeedbackMs":43200000,"minUserTurnsBeforeFeedback":5,"minUserTurnsBetweenFeedback":25,"hideThanksAfterMs":3000,"onForModels":["*"],"probability":0.05,"lastSurveyShownTime":null}}}}} {"dir":"out","ms":1160,"frame":{"type":"control_response","response":{"subtype":"success","request_id":"5v6gd7b4kd6"}}} {"dir":"in","ms":1161,"frame":{"request_id":"zpcjtu4elhn","type":"control_request","request":{"subtype":"set_permission_mode","mode":"dontAsk"}}}