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
7 changes: 7 additions & 0 deletions src/local-agent-claude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 19 additions & 1 deletion src/local-agent-claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(/**)",
Expand Down Expand Up @@ -386,7 +404,7 @@ function claudeAuthorityOptions(

const permissions = {
defaultMode: "dontAsk",
deny: allowed ? [] : ["Bash", "Edit"],
deny: allowed ? [] : [...CLAUDE_READ_ONLY_DENIED_TOOLS],
};

const sandbox = {
Expand Down
4 changes: 2 additions & 2 deletions test/agents-replay/transcripts/claude/read-only.jsonl
Original file line number Diff line number Diff line change
@@ -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"}}}
Expand Down
Loading