



















@@ -1,13 +1,22 @@
11import path from "node:path";
22import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
3+import { resolveExecDefaults } from "../agents/exec-defaults.js";
4+import { normalizeProviderId } from "../agents/provider-id.js";
35import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js";
46import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
57import type { ConfigFileSnapshot, OpenClawConfig } from "../config/config.js";
68import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
9+import type { CliBackendConfig } from "../config/types.agent-defaults.js";
710import type { GatewayAuthConfig } from "../config/types.gateway.js";
811import type { SecurityAuditSuppression } from "../config/types.openclaw.js";
912import { isInterpreterLikeAllowlistPattern } from "../infra/command-analysis/inline-eval.js";
10-import { type ExecApprovalsFile, loadExecApprovals } from "../infra/exec-approvals.js";
13+import {
14+type ExecApprovalsFile,
15+loadExecApprovals,
16+maxAsk,
17+minSecurity,
18+resolveExecApprovalsFromFile,
19+} from "../infra/exec-approvals.js";
1120import {
1221listInterpreterLikeSafeBins,
1322resolveMergedSafeBinProfileFixtures,
@@ -42,6 +51,10 @@ type SecurityAuditExplicitGatewayAuth = {
4251password?: string;
4352};
4453type SecurityAuditGatewayAuthOverride = Pick<GatewayAuthConfig, "mode" | "token" | "password">;
54+type ClaudePermissionModeHit = {
55+argSet: "args" | "resumeArgs";
56+mode: string;
57+};
45584659export type {
4760SecurityAuditFinding,
@@ -559,13 +572,127 @@ export function collectElevatedFindings(cfg: OpenClawConfig): SecurityAuditFindi
559572return findings;
560573}
561574575+const CLAUDE_PERMISSION_MODE_FLAG = "--permission-mode";
576+const CLAUDE_BYPASS_PERMISSION_MODE = "bypassPermissions";
577+578+function extractClaudePermissionMode(args: readonly string[] | undefined): string | undefined {
579+if (!Array.isArray(args)) {
580+return undefined;
581+}
582+for (let i = args.length - 1; i >= 0; i -= 1) {
583+const arg = args[i] ?? "";
584+if (arg === CLAUDE_PERMISSION_MODE_FLAG) {
585+const value = args[i + 1];
586+if (typeof value === "string" && value.trim().length > 0 && !value.startsWith("-")) {
587+return value.trim();
588+}
589+continue;
590+}
591+if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_FLAG}=`)) {
592+const value = arg.slice(`${CLAUDE_PERMISSION_MODE_FLAG}=`.length).trim();
593+if (value.length > 0 && !value.startsWith("-")) {
594+return value;
595+}
596+}
597+}
598+return undefined;
599+}
600+601+function collectRestrictiveClaudePermissionModeHits(
602+backend: CliBackendConfig | undefined,
603+): ClaudePermissionModeHit[] {
604+if (!isManagedClaudeLiveBackendConfig(backend)) {
605+return [];
606+}
607+const hits: ClaudePermissionModeHit[] = [];
608+const argsMode = extractClaudePermissionMode(backend.args);
609+if (argsMode && argsMode !== CLAUDE_BYPASS_PERMISSION_MODE) {
610+hits.push({ argSet: "args", mode: argsMode });
611+}
612+const resumeArgsMode = extractClaudePermissionMode(backend.resumeArgs);
613+if (resumeArgsMode && resumeArgsMode !== CLAUDE_BYPASS_PERMISSION_MODE) {
614+hits.push({ argSet: "resumeArgs", mode: resumeArgsMode });
615+}
616+return hits;
617+}
618+619+function isManagedClaudeLiveBackendConfig(
620+backend: CliBackendConfig | undefined,
621+): backend is CliBackendConfig {
622+if (!backend) {
623+return false;
624+}
625+const output = backend.output ?? "jsonl";
626+const input = backend.input ?? "stdin";
627+const liveSession =
628+backend.liveSession ?? (output === "jsonl" && input === "stdin" ? "claude-stdio" : undefined);
629+return liveSession === "claude-stdio" && output === "jsonl" && input === "stdin";
630+}
631+632+function findClaudeCliBackendConfig(
633+backends: Record<string, CliBackendConfig> | undefined,
634+): CliBackendConfig | undefined {
635+if (!backends) {
636+return undefined;
637+}
638+const directKey = Object.keys(backends).find(
639+(key) => normalizeOptionalLowercaseString(key) === "claude-cli",
640+);
641+if (directKey) {
642+return backends[directKey];
643+}
644+for (const [key, backend] of Object.entries(backends)) {
645+if (normalizeProviderId(key) === "claude-cli") {
646+return backend;
647+}
648+}
649+return undefined;
650+}
651+652+function collectYoloExecScopeIds(cfg: OpenClawConfig, approvals: ExecApprovalsFile): string[] {
653+const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
654+return [
655+{ id: DEFAULT_AGENT_ID },
656+ ...agents
657+.filter(
658+(entry): entry is NonNullable<(typeof agents)[number]> =>
659+Boolean(entry) && typeof entry === "object" && typeof entry.id === "string",
660+)
661+.map((entry) => ({ id: entry.id })),
662+]
663+.filter((entry) => {
664+const execDefaults = resolveExecDefaults({
665+ cfg,
666+agentId: entry.id === DEFAULT_AGENT_ID ? undefined : entry.id,
667+});
668+const resolvedApprovals = resolveExecApprovalsFromFile({
669+file: approvals,
670+agentId: entry.id === DEFAULT_AGENT_ID ? undefined : entry.id,
671+overrides: {
672+security: execDefaults.security,
673+ask: execDefaults.ask,
674+},
675+});
676+return (
677+minSecurity(execDefaults.security, resolvedApprovals.agent.security) === "full" &&
678+maxAsk(execDefaults.ask, resolvedApprovals.agent.ask) === "off"
679+);
680+})
681+.map((entry) => entry.id);
682+}
683+562684export function collectExecRuntimeFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
563685const findings: SecurityAuditFinding[] = [];
564686const globalExecHost = cfg.tools?.exec?.host;
565687const globalStrictInlineEval = cfg.tools?.exec?.strictInlineEval === true;
566688const defaultSandboxMode = resolveSandboxConfigForAgent(cfg).mode;
567689const defaultHostIsExplicitSandbox = globalExecHost === "sandbox";
568690const approvals = loadExecApprovals();
691+const claudePermissionModeHits = collectRestrictiveClaudePermissionModeHits(
692+findClaudeCliBackendConfig(cfg.agents?.defaults?.cliBackends),
693+);
694+const yoloExecScopeIds =
695+claudePermissionModeHits.length > 0 ? collectYoloExecScopeIds(cfg, approvals) : [];
569696570697if (defaultHostIsExplicitSandbox && defaultSandboxMode === "off") {
571698findings.push({
@@ -646,6 +773,17 @@ export function collectExecRuntimeFindings(cfg: OpenClawConfig): SecurityAuditFi
646773});
647774}
648775776+if (claudePermissionModeHits.length > 0 && yoloExecScopeIds.length > 0) {
777+findings.push({
778+checkId: "agents.claude_cli.permission_mode_overridden_by_yolo",
779+severity: "warn",
780+title: "Claude permission mode is ignored under YOLO exec",
781+detail: `claude-cli sets ${claudePermissionModeHits.map((hit) => `${hit.argSet}=${hit.mode}`).join(", ")}, but OpenClaw exec is YOLO for: ${yoloExecScopeIds.join(", ")}. Managed Claude live sessions use --permission-mode bypassPermissions.`,
782+remediation:
783+"Restrict OpenClaw tools.exec.security/tools.exec.ask, or remove the Claude --permission-mode override.",
784+});
785+}
786+649787if (openExecSurfacePaths.length > 0 && execEnabledScopes.length > 0) {
650788findings.push({
651789checkId: "security.exposure.open_channels_with_exec",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。