Skip to content

Commit 7587e4c

Browse files
cathrynlaverythewilloftheshadowvincentkoc
authored
fix: ensure bypassPermissions when custom CLI backend args override defaults (openclaw#61114)
* fix: ensure bypassPermissions on custom CLI backend args When users override cliBackends.claude-cli.args (e.g. to add --verbose or change --output-format), the override array replaces the default entirely. The normalization step only re-added --permission-mode bypassPermissions when the legacy --dangerously-skip-permissions flag was present — if neither flag existed, it did nothing. This causes cron and heartbeat runs to silently fail with "exec denied: Cron runs cannot wait for interactive exec approval" because the CLI subprocess launches in interactive permission mode. Fix: always inject --permission-mode bypassPermissions when no explicit permission-mode flag is found in the resolved args, regardless of whether the legacy flag was present. * test(anthropic): add claude-cli permission normalization coverage * fix(test-utils): include video generation providers * fix: preserve claude-cli bypassPermissions on custom args (openclaw#61114) (thanks @cathrynlavery) --------- Co-authored-by: Shadow <hi@shadowing.dev> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
1 parent 91ddf38 commit 7587e4c

4 files changed

Lines changed: 116 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai
116116
- MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.
117117
- CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010.
118118
- Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more `/` entries visible. (#61129) Thanks @neeravmakwana.
119+
- Agents/Claude CLI: keep non-interactive `--permission-mode bypassPermissions` when custom `cliBackends.claude-cli.args` override defaults, so cron and heartbeat Claude CLI runs do not regress to interactive approval mode. (#61114) Thanks @cathrynlavery and @thewilloftheshadow.
119120

120121
## 2026.4.2
121122

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, expect, it } from "vitest";
2+
import { buildAnthropicCliBackend } from "./cli-backend.js";
3+
import { normalizeClaudeBackendConfig, normalizeClaudePermissionArgs } from "./cli-shared.js";
4+
5+
describe("normalizeClaudePermissionArgs", () => {
6+
it("injects bypassPermissions when args omit permission flags", () => {
7+
expect(
8+
normalizeClaudePermissionArgs(["-p", "--output-format", "stream-json", "--verbose"]),
9+
).toEqual([
10+
"-p",
11+
"--output-format",
12+
"stream-json",
13+
"--verbose",
14+
"--permission-mode",
15+
"bypassPermissions",
16+
]);
17+
});
18+
19+
it("removes legacy skip-permissions and injects bypassPermissions", () => {
20+
expect(
21+
normalizeClaudePermissionArgs(["-p", "--dangerously-skip-permissions", "--verbose"]),
22+
).toEqual(["-p", "--verbose", "--permission-mode", "bypassPermissions"]);
23+
});
24+
25+
it("keeps explicit permission-mode overrides", () => {
26+
expect(normalizeClaudePermissionArgs(["-p", "--permission-mode", "acceptEdits"])).toEqual([
27+
"-p",
28+
"--permission-mode",
29+
"acceptEdits",
30+
]);
31+
expect(normalizeClaudePermissionArgs(["-p", "--permission-mode=acceptEdits"])).toEqual([
32+
"-p",
33+
"--permission-mode=acceptEdits",
34+
]);
35+
});
36+
});
37+
38+
describe("normalizeClaudeBackendConfig", () => {
39+
it("normalizes both args and resumeArgs for custom overrides", () => {
40+
const normalized = normalizeClaudeBackendConfig({
41+
command: "claude",
42+
args: ["-p", "--output-format", "stream-json", "--verbose"],
43+
resumeArgs: ["-p", "--output-format", "stream-json", "--verbose", "--resume", "{sessionId}"],
44+
});
45+
46+
expect(normalized.args).toEqual([
47+
"-p",
48+
"--output-format",
49+
"stream-json",
50+
"--verbose",
51+
"--permission-mode",
52+
"bypassPermissions",
53+
]);
54+
expect(normalized.resumeArgs).toEqual([
55+
"-p",
56+
"--output-format",
57+
"stream-json",
58+
"--verbose",
59+
"--resume",
60+
"{sessionId}",
61+
"--permission-mode",
62+
"bypassPermissions",
63+
]);
64+
});
65+
66+
it("is wired through the anthropic cli backend normalize hook", () => {
67+
const backend = buildAnthropicCliBackend();
68+
const normalizeConfig = backend.normalizeConfig;
69+
70+
expect(normalizeConfig).toBeTypeOf("function");
71+
72+
const normalized = normalizeConfig?.({
73+
...backend.config,
74+
args: ["-p", "--output-format", "stream-json", "--verbose"],
75+
resumeArgs: ["-p", "--output-format", "stream-json", "--verbose", "--resume", "{sessionId}"],
76+
});
77+
78+
expect(normalized?.args).toContain("--permission-mode");
79+
expect(normalized?.args).toContain("bypassPermissions");
80+
expect(normalized?.resumeArgs).toContain("--permission-mode");
81+
expect(normalized?.resumeArgs).toContain("bypassPermissions");
82+
});
83+
});

extensions/anthropic/cli-shared.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,10 @@ export function normalizeClaudePermissionArgs(args?: string[]): string[] | undef
4646
return args;
4747
}
4848
const normalized: string[] = [];
49-
let sawLegacySkip = false;
5049
let hasPermissionMode = false;
5150
for (let i = 0; i < args.length; i += 1) {
5251
const arg = args[i];
5352
if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) {
54-
sawLegacySkip = true;
5553
continue;
5654
}
5755
if (arg === CLAUDE_PERMISSION_MODE_ARG) {
@@ -69,7 +67,7 @@ export function normalizeClaudePermissionArgs(args?: string[]): string[] | undef
6967
}
7068
normalized.push(arg);
7169
}
72-
if (sawLegacySkip && !hasPermissionMode) {
70+
if (!hasPermissionMode) {
7371
normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE);
7472
}
7573
return normalized;

src/agents/cli-backends.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,37 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
300300
expect(resolved?.config.resumeArgs).not.toContain("bypassPermissions");
301301
});
302302

303+
it("injects bypassPermissions when custom args omit any permission flag", () => {
304+
const cfg = {
305+
agents: {
306+
defaults: {
307+
cliBackends: {
308+
"claude-cli": {
309+
command: "claude",
310+
args: ["-p", "--output-format", "stream-json", "--verbose"],
311+
resumeArgs: [
312+
"-p",
313+
"--output-format",
314+
"stream-json",
315+
"--verbose",
316+
"--resume",
317+
"{sessionId}",
318+
],
319+
},
320+
},
321+
},
322+
},
323+
} satisfies OpenClawConfig;
324+
325+
const resolved = resolveCliBackendConfig("claude-cli", cfg);
326+
327+
expect(resolved).not.toBeNull();
328+
expect(resolved?.config.args).toContain("--permission-mode");
329+
expect(resolved?.config.args).toContain("bypassPermissions");
330+
expect(resolved?.config.resumeArgs).toContain("--permission-mode");
331+
expect(resolved?.config.resumeArgs).toContain("bypassPermissions");
332+
});
333+
303334
it("keeps bundle MCP enabled for override-only claude-cli config when the plugin registry is absent", () => {
304335
const registry = createEmptyPluginRegistry();
305336
setActivePluginRegistry(registry);

0 commit comments

Comments
 (0)