From a4a5920574bbfa625b25e21f76d7499d260286fb Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Wed, 17 Dec 2025 18:31:35 -0800 Subject: [PATCH] =?UTF-8?q?fix:=20generate=20pattern=20rules=20for=20safe?= =?UTF-8?q?=20commands=20in=20compound=20bash=20expres=E2=80=A6=20(#275)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Letta --- src/permissions/analyzer.ts | 94 +++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/src/permissions/analyzer.ts b/src/permissions/analyzer.ts index a169ca0..63af1e0 100644 --- a/src/permissions/analyzer.ts +++ b/src/permissions/analyzer.ts @@ -178,6 +178,54 @@ function analyzeEditApproval( /** * Analyze Bash command approval */ +// Safe read-only commands that can be pattern-matched +const SAFE_READONLY_COMMANDS = [ + "ls", + "cat", + "pwd", + "echo", + "which", + "type", + "whoami", + "date", + "grep", + "find", + "head", + "tail", + "wc", + "diff", + "file", + "stat", +]; + +// Commands that should never be auto-approved +const DANGEROUS_COMMANDS = [ + "rm", + "mv", + "chmod", + "chown", + "sudo", + "dd", + "mkfs", + "fdisk", + "kill", + "killall", +]; + +/** + * Check if a compound command contains any dangerous commands + */ +function containsDangerousCommand(command: string): boolean { + const segments = command.split(/\s*(?:&&|\||;)\s*/); + for (const segment of segments) { + const baseCmd = segment.trim().split(/\s+/)[0] || ""; + if (DANGEROUS_COMMANDS.includes(baseCmd)) { + return true; + } + } + return false; +} + function analyzeBashApproval( command: string, _workingDir: string, @@ -186,21 +234,8 @@ function analyzeBashApproval( const baseCommand = parts[0] || ""; const firstArg = parts[1] || ""; - // Dangerous commands - no persistence - const dangerousCommands = [ - "rm", - "mv", - "chmod", - "chown", - "sudo", - "dd", - "mkfs", - "fdisk", - "kill", - "killall", - ]; - - if (baseCommand && dangerousCommands.includes(baseCommand)) { + // Check if command contains ANY dangerous commands (including in pipelines) + if (containsDangerousCommand(command)) { return { recommendedRule: "", ruleDescription: "", @@ -302,22 +337,7 @@ function analyzeBashApproval( } // Safe read-only commands - const safeCommands = [ - "ls", - "cat", - "pwd", - "echo", - "which", - "type", - "whoami", - "date", - "grep", - "find", - "head", - "tail", - ]; - - if (baseCommand && safeCommands.includes(baseCommand)) { + if (baseCommand && SAFE_READONLY_COMMANDS.includes(baseCommand)) { return { recommendedRule: `Bash(${baseCommand}:*)`, ruleDescription: `'${baseCommand}' commands`, @@ -400,6 +420,18 @@ function analyzeBashApproval( }; } } + + // Check if this segment is a safe read-only command + if (segmentBase && SAFE_READONLY_COMMANDS.includes(segmentBase)) { + return { + recommendedRule: `Bash(${segmentBase}:*)`, + ruleDescription: `'${segmentBase}' commands`, + approveAlwaysText: `Yes, and don't ask again for '${segmentBase}' commands in this project`, + defaultScope: "project", + allowPersistence: true, + safetyLevel: "safe", + }; + } } }