From 7d30894c4a40291deaebfba522e693f58f1780f8 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 23 Apr 2026 12:02:45 -0700 Subject: [PATCH] fix(anthropic): stop forcing claude permission bypass --- CHANGELOG.md | 1 + extensions/anthropic/cli-backend.ts | 4 -- extensions/anthropic/cli-shared.test.ts | 31 +++++--------- extensions/anthropic/cli-shared.ts | 12 +++--- src/agents/cli-backends.test.ts | 57 ++++++++----------------- 5 files changed, 34 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5faa731e9b..1514f8ec4e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Anthropic/CLI security: stop Claude CLI backend defaults from forcing `bypassPermissions`, and strip malformed permission-mode overrides instead of silently falling back to a bypass. (#70723) Thanks @vincentkoc. - Android/security: require loopback-only cleartext gateway connections on Android manual and scanned routes, so private-LAN and link-local `ws://` endpoints now fail closed unless TLS is enabled. (#70722) Thanks @vincentkoc. - Pairing/security: require private-IP or loopback hosts for cleartext mobile pairing, and stop treating `.local` or dotless hostnames as safe cleartext endpoints. (#70721) Thanks @vincentkoc. - Approvals/security: require explicit chat exec-approval enablement instead of auto-enabling approval clients just because approvers resolve from config or owner allowlists. (#70715) Thanks @vincentkoc. diff --git a/extensions/anthropic/cli-backend.ts b/extensions/anthropic/cli-backend.ts index 2933fa27741..26d0247e0e6 100644 --- a/extensions/anthropic/cli-backend.ts +++ b/extensions/anthropic/cli-backend.ts @@ -36,8 +36,6 @@ export function buildAnthropicCliBackend(): CliBackendPlugin { "--verbose", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", ], resumeArgs: [ "-p", @@ -47,8 +45,6 @@ export function buildAnthropicCliBackend(): CliBackendPlugin { "--verbose", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", "--resume", "{sessionId}", ], diff --git a/extensions/anthropic/cli-shared.test.ts b/extensions/anthropic/cli-shared.test.ts index 306315e53bb..57c6f14f185 100644 --- a/extensions/anthropic/cli-shared.test.ts +++ b/extensions/anthropic/cli-shared.test.ts @@ -8,23 +8,16 @@ import { } from "./cli-shared.js"; describe("normalizeClaudePermissionArgs", () => { - it("injects bypassPermissions when args omit permission flags", () => { + it("leaves args alone when they omit permission flags", () => { expect( normalizeClaudePermissionArgs(["-p", "--output-format", "stream-json", "--verbose"]), - ).toEqual([ - "-p", - "--output-format", - "stream-json", - "--verbose", - "--permission-mode", - "bypassPermissions", - ]); + ).toEqual(["-p", "--output-format", "stream-json", "--verbose"]); }); - it("removes legacy skip-permissions and injects bypassPermissions", () => { + it("removes legacy skip-permissions without adding bypassPermissions", () => { expect( normalizeClaudePermissionArgs(["-p", "--dangerously-skip-permissions", "--verbose"]), - ).toEqual(["-p", "--verbose", "--permission-mode", "bypassPermissions"]); + ).toEqual(["-p", "--verbose"]); }); it("keeps explicit permission-mode overrides", () => { @@ -39,10 +32,14 @@ describe("normalizeClaudePermissionArgs", () => { ]); }); - it("treats a bare permission-mode flag as malformed and falls back to bypassPermissions", () => { + it("drops malformed permission-mode flags in both split and equals forms", () => { expect( normalizeClaudePermissionArgs(["-p", "--permission-mode", "--output-format", "stream-json"]), - ).toEqual(["-p", "--output-format", "stream-json", "--permission-mode", "bypassPermissions"]); + ).toEqual(["-p", "--output-format", "stream-json"]); + expect(normalizeClaudePermissionArgs(["-p", "--permission-mode="])).toEqual(["-p"]); + expect(normalizeClaudePermissionArgs(["-p", "--permission-mode=--output-format"])).toEqual([ + "-p", + ]); }); }); @@ -92,8 +89,6 @@ describe("normalizeClaudeBackendConfig", () => { "--verbose", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", ]); expect(normalized.resumeArgs).toEqual([ "-p", @@ -104,8 +99,6 @@ describe("normalizeClaudeBackendConfig", () => { "{sessionId}", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", ]); expect(normalized.output).toBe("jsonl"); expect(normalized.liveSession).toBe("claude-stdio"); @@ -136,12 +129,8 @@ describe("normalizeClaudeBackendConfig", () => { resumeArgs: ["-p", "--output-format", "stream-json", "--verbose", "--resume", "{sessionId}"], }); - expect(normalized?.args).toContain("--permission-mode"); - expect(normalized?.args).toContain("bypassPermissions"); expect(normalized?.args).toContain("--setting-sources"); expect(normalized?.args).toContain("user"); - expect(normalized?.resumeArgs).toContain("--permission-mode"); - expect(normalized?.resumeArgs).toContain("bypassPermissions"); expect(normalized?.resumeArgs).toContain("--setting-sources"); expect(normalized?.resumeArgs).toContain("user"); expect(normalized?.liveSession).toBe("claude-stdio"); diff --git a/extensions/anthropic/cli-shared.ts b/extensions/anthropic/cli-shared.ts index 44cab5c54f1..ef698e0f9e1 100644 --- a/extensions/anthropic/cli-shared.ts +++ b/extensions/anthropic/cli-shared.ts @@ -56,7 +56,6 @@ export const CLAUDE_CLI_CLEAR_ENV = [ const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions"; const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode"; -const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions"; const CLAUDE_SETTING_SOURCES_ARG = "--setting-sources"; const CLAUDE_SAFE_SETTING_SOURCES = "user"; @@ -69,7 +68,6 @@ export function normalizeClaudePermissionArgs(args?: string[]): string[] | undef return args; } const normalized: string[] = []; - let hasPermissionMode = false; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) { @@ -82,7 +80,6 @@ export function normalizeClaudePermissionArgs(args?: string[]): string[] | undef maybeValue.trim().length > 0 && !maybeValue.startsWith("-") ) { - hasPermissionMode = true; normalized.push(arg); normalized.push(maybeValue); i += 1; @@ -90,13 +87,14 @@ export function normalizeClaudePermissionArgs(args?: string[]): string[] | undef continue; } if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) { - hasPermissionMode = true; + const maybeValue = arg.slice(`${CLAUDE_PERMISSION_MODE_ARG}=`.length).trim(); + if (maybeValue.length > 0 && !maybeValue.startsWith("-")) { + normalized.push(`${CLAUDE_PERMISSION_MODE_ARG}=${maybeValue}`); + } + continue; } normalized.push(arg); } - if (!hasPermissionMode) { - normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE); - } return normalized; } diff --git a/src/agents/cli-backends.test.ts b/src/agents/cli-backends.test.ts index d81d98217db..10252cc9f92 100644 --- a/src/agents/cli-backends.test.ts +++ b/src/agents/cli-backends.test.ts @@ -101,8 +101,6 @@ const NORMALIZED_CLAUDE_FALLBACK_ARGS = [ "stream-json", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", ]; const NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS = [ @@ -111,8 +109,6 @@ const NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS = [ "{sessionId}", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", ]; function normalizeTestClaudeArgs(args?: string[]): string[] | undefined { @@ -121,7 +117,6 @@ function normalizeTestClaudeArgs(args?: string[]): string[] | undefined { } const normalized: string[] = []; let hasSettingSources = false; - let hasPermissionMode = false; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; if (arg === "--dangerously-skip-permissions") { @@ -144,23 +139,23 @@ function normalizeTestClaudeArgs(args?: string[]): string[] | undefined { if (arg === "--permission-mode") { const maybeValue = args[i + 1]; if (maybeValue && !maybeValue.startsWith("-")) { - hasPermissionMode = true; normalized.push(arg, maybeValue); i += 1; } continue; } if (arg.startsWith("--permission-mode=")) { - hasPermissionMode = true; + const maybeValue = arg.slice("--permission-mode=".length).trim(); + if (maybeValue.length > 0 && !maybeValue.startsWith("-")) { + normalized.push(`--permission-mode=${maybeValue}`); + } + continue; } normalized.push(arg); } if (!hasSettingSources) { normalized.push("--setting-sources", "user"); } - if (!hasPermissionMode) { - normalized.push("--permission-mode", "bypassPermissions"); - } return normalized; } @@ -191,8 +186,6 @@ beforeEach(() => { "--verbose", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", ], resumeArgs: [ "stream-json", @@ -200,8 +193,6 @@ beforeEach(() => { "--verbose", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", "--resume", "{sessionId}", ], @@ -414,7 +405,7 @@ describe("resolveCliBackendLiveTest", () => { }); describe("resolveCliBackendConfig claude-cli defaults", () => { - it("uses non-interactive permission-mode defaults for fresh and resume args", () => { + it("keeps user-only setting sources without forcing a permission-mode default", () => { const resolved = resolveCliBackendConfig("claude-cli"); expect(resolved).not.toBeNull(); @@ -426,8 +417,7 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved?.config.args).toContain("--verbose"); expect(resolved?.config.args).toContain("--setting-sources"); expect(resolved?.config.args).toContain("user"); - expect(resolved?.config.args).toContain("--permission-mode"); - expect(resolved?.config.args).toContain("bypassPermissions"); + expect(resolved?.config.args).not.toContain("--permission-mode"); expect(resolved?.config.args).not.toContain("--dangerously-skip-permissions"); expect(resolved?.config.input).toBe("stdin"); expect(resolved?.config.resumeArgs).toContain("stream-json"); @@ -435,8 +425,7 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved?.config.resumeArgs).toContain("--verbose"); expect(resolved?.config.resumeArgs).toContain("--setting-sources"); expect(resolved?.config.resumeArgs).toContain("user"); - expect(resolved?.config.resumeArgs).toContain("--permission-mode"); - expect(resolved?.config.resumeArgs).toContain("bypassPermissions"); + expect(resolved?.config.resumeArgs).not.toContain("--permission-mode"); expect(resolved?.config.resumeArgs).not.toContain("--dangerously-skip-permissions"); }); @@ -459,12 +448,10 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved?.config.command).toBe("/usr/local/bin/claude"); expect(resolved?.config.args).toContain("--setting-sources"); expect(resolved?.config.args).toContain("user"); - expect(resolved?.config.args).toContain("--permission-mode"); - expect(resolved?.config.args).toContain("bypassPermissions"); + expect(resolved?.config.args).not.toContain("--permission-mode"); expect(resolved?.config.resumeArgs).toContain("--setting-sources"); expect(resolved?.config.resumeArgs).toContain("user"); - expect(resolved?.config.resumeArgs).toContain("--permission-mode"); - expect(resolved?.config.resumeArgs).toContain("bypassPermissions"); + expect(resolved?.config.resumeArgs).not.toContain("--permission-mode"); expect(resolved?.config.env).not.toHaveProperty("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST"); expect(resolved?.config.clearEnv).toContain("ANTHROPIC_API_TOKEN"); expect(resolved?.config.clearEnv).toContain("ANTHROPIC_BASE_URL"); @@ -478,7 +465,7 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved?.config.clearEnv).toContain("CLAUDE_CODE_USE_COWORK_PLUGINS"); }); - it("normalizes legacy skip-permissions overrides to permission-mode bypassPermissions", () => { + it("drops legacy skip-permissions overrides without inventing bypassPermissions", () => { const cfg = { agents: { defaults: { @@ -504,11 +491,9 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved).not.toBeNull(); expect(resolved?.config.args).not.toContain("--dangerously-skip-permissions"); - expect(resolved?.config.args).toContain("--permission-mode"); - expect(resolved?.config.args).toContain("bypassPermissions"); + expect(resolved?.config.args).not.toContain("--permission-mode"); expect(resolved?.config.resumeArgs).not.toContain("--dangerously-skip-permissions"); - expect(resolved?.config.resumeArgs).toContain("--permission-mode"); - expect(resolved?.config.resumeArgs).toContain("bypassPermissions"); + expect(resolved?.config.resumeArgs).not.toContain("--permission-mode"); }); it("keeps explicit permission-mode overrides while removing legacy skip flag", () => { @@ -610,11 +595,11 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved?.config.resumeArgs).toEqual(NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS); }); - it("falls back to bypassPermissions when a custom override leaves permission-mode without a value", () => { + it("drops malformed permission-mode overrides without adding bypassPermissions", () => { const cfg = createClaudeCliOverrideConfig({ command: "claude", args: ["-p", "--permission-mode", "--output-format", "stream-json"], - resumeArgs: ["-p", "--permission-mode", "--resume", "{sessionId}"], + resumeArgs: ["-p", "--permission-mode=--resume", "--resume", "{sessionId}"], }); const resolved = resolveCliBackendConfig("claude-cli", cfg); @@ -624,7 +609,7 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved?.config.resumeArgs).toEqual(NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS); }); - it("injects bypassPermissions when custom args omit any permission flag", () => { + it("leaves permission-mode unset when custom args omit it", () => { const cfg = { agents: { defaults: { @@ -651,12 +636,10 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved).not.toBeNull(); expect(resolved?.config.args).toContain("--setting-sources"); expect(resolved?.config.args).toContain("user"); - expect(resolved?.config.args).toContain("--permission-mode"); - expect(resolved?.config.args).toContain("bypassPermissions"); + expect(resolved?.config.args).not.toContain("--permission-mode"); expect(resolved?.config.resumeArgs).toContain("--setting-sources"); expect(resolved?.config.resumeArgs).toContain("user"); - expect(resolved?.config.resumeArgs).toContain("--permission-mode"); - expect(resolved?.config.resumeArgs).toContain("bypassPermissions"); + expect(resolved?.config.resumeArgs).not.toContain("--permission-mode"); }); it("keeps hardened clearEnv defaults when custom claude env overrides are merged", () => { @@ -723,8 +706,6 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { "json", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", ]); expect(resolved?.config.resumeArgs).toEqual([ "-p", @@ -734,8 +715,6 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { "{sessionId}", "--setting-sources", "user", - "--permission-mode", - "bypassPermissions", ]); expect(resolved?.config.systemPromptArg).toBe("--append-system-prompt"); expect(resolved?.config.systemPromptWhen).toBe("first");