From 01acc4c10e3f23771a8b4c6c255f3d78e25381f1 Mon Sep 17 00:00:00 2001 From: KRATOS <84986124+gnanam1990@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:23:09 +0530 Subject: [PATCH] fix: auto-allow safe read-only commands in acceptEdits mode (#341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: auto-allow safe read-only commands in acceptEdits mode In acceptEdits mode, read-only commands like grep, cat, ls, find, head, tail were still prompting for approval. This created unnecessary friction since these commands cannot modify or delete files. Add safe read-only commands to ACCEPT_EDITS_ALLOWED_COMMANDS: grep, cat, ls, find, head, tail, echo, pwd, wc, sort, uniq, diff These are all read-only — they cannot cause data loss or modify the filesystem. Auto-allowing them reduces approval fatigue in acceptEdits mode without introducing any safety risk. Write commands (rm, rmdir, mv, cp, sed, mkdir, touch) are unchanged. The dangerous path guard for rm/rmdir remains in place. Fixes #251. Co-Authored-By: Claude Sonnet 4.6 * fix(bash): block unsafe acceptEdits auto-allow Keep the new read-only acceptEdits commands behind the existing read-only validator and block shell redirection based on the original command text. This prevents commands like echo > file and find -delete from being silently auto-approved while preserving safe read-only commands. Co-Authored-By: Claude --------- Co-authored-by: Claude Sonnet 4.6 --- src/tools/BashTool/modeValidation.test.ts | 44 ++++++++++ src/tools/BashTool/modeValidation.ts | 101 ++++++++++++++++++++-- 2 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 src/tools/BashTool/modeValidation.test.ts diff --git a/src/tools/BashTool/modeValidation.test.ts b/src/tools/BashTool/modeValidation.test.ts new file mode 100644 index 00000000..cb35a7c0 --- /dev/null +++ b/src/tools/BashTool/modeValidation.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from 'bun:test' +import { getEmptyToolPermissionContext } from '../../Tool.js' +import { checkPermissionMode } from './modeValidation.js' + +const acceptEditsContext = { + ...getEmptyToolPermissionContext(), + mode: 'acceptEdits' as const, +} + +test('acceptEdits does not auto-allow read commands with output redirection', () => { + const result = checkPermissionMode( + { command: 'echo hello > output.txt' } as never, + acceptEditsContext, + ) + + expect(result.behavior).toBe('passthrough') +}) + +test('acceptEdits does not auto-allow mutating find invocations', () => { + const result = checkPermissionMode( + { command: 'find . -delete' } as never, + acceptEditsContext, + ) + + expect(result.behavior).toBe('passthrough') +}) + +test('acceptEdits still auto-allows safe read-only commands', () => { + const result = checkPermissionMode( + { command: 'grep foo package.json' } as never, + acceptEditsContext, + ) + + expect(result.behavior).toBe('allow') +}) + +test('acceptEdits still blocks dangerous rm paths even in auto-allow mode', () => { + const result = checkPermissionMode( + { command: 'rm -rf ~' } as never, + acceptEditsContext, + ) + + expect(result.behavior).toBe('ask') +}) diff --git a/src/tools/BashTool/modeValidation.ts b/src/tools/BashTool/modeValidation.ts index 5e5147d1..e8735ef1 100644 --- a/src/tools/BashTool/modeValidation.ts +++ b/src/tools/BashTool/modeValidation.ts @@ -1,12 +1,15 @@ import type { z } from 'zod/v4' import type { ToolPermissionContext } from '../../Tool.js' import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js' +import { tryParseShellCommand } from '../../utils/bash/shellQuote.js' import { getCwd } from '../../utils/cwd.js' import type { PermissionResult } from '../../utils/permissions/PermissionResult.js' import type { BashTool } from './BashTool.js' +import { checkReadOnlyConstraints } from './readOnlyValidation.js' import { checkDangerousRemovalPaths } from './pathValidation.js' -const ACCEPT_EDITS_ALLOWED_COMMANDS = [ +const ACCEPT_EDITS_WRITE_COMMANDS = [ + // Filesystem write commands 'mkdir', 'touch', 'rm', @@ -14,17 +17,66 @@ const ACCEPT_EDITS_ALLOWED_COMMANDS = [ 'mv', 'cp', 'sed', + ] as const + +const ACCEPT_EDITS_READ_ONLY_COMMANDS = [ + // Safe read-only commands — cannot modify files or cause data loss. + // These still need to pass the existing read-only validator so redirects and + // dangerous flags fall through to the normal permission flow. + 'grep', + 'cat', + 'ls', + 'find', + 'head', + 'tail', + 'echo', + 'pwd', + 'wc', + 'sort', + 'uniq', + 'diff', ] as const -type FilesystemCommand = (typeof ACCEPT_EDITS_ALLOWED_COMMANDS)[number] +type AcceptEditsWriteCommand = (typeof ACCEPT_EDITS_WRITE_COMMANDS)[number] +type AcceptEditsReadOnlyCommand = + (typeof ACCEPT_EDITS_READ_ONLY_COMMANDS)[number] -function isFilesystemCommand(command: string): command is FilesystemCommand { - return ACCEPT_EDITS_ALLOWED_COMMANDS.includes(command as FilesystemCommand) +function isAcceptEditsWriteCommand( + command: string, +): command is AcceptEditsWriteCommand { + return ACCEPT_EDITS_WRITE_COMMANDS.includes(command as AcceptEditsWriteCommand) +} + +function isAcceptEditsReadOnlyCommand( + command: string, +): command is AcceptEditsReadOnlyCommand { + return ACCEPT_EDITS_READ_ONLY_COMMANDS.includes( + command as AcceptEditsReadOnlyCommand, + ) +} + +function hasShellRedirection(cmd: string): boolean { + const parsed = tryParseShellCommand(cmd, env => `$${env}`) + if (!parsed.success) { + // Fail closed: unparseable commands should go through the normal prompt flow. + return true + } + + return parsed.tokens.some( + token => + typeof token === 'object' && + token !== null && + 'op' in token && + ['>', '>>', '>|', '&>', '&>>', '1>', '1>>', '2>', '2>>'].includes( + String(token.op), + ), + ) } function validateCommandForMode( cmd: string, toolPermissionContext: ToolPermissionContext, + originalInput: string, ): PermissionResult { const trimmedCmd = cmd.trim() const [baseCmd] = trimmedCmd.split(/\s+/) @@ -36,10 +88,10 @@ function validateCommandForMode( } } - // In Accept Edits mode, auto-allow filesystem operations + // In Accept Edits mode, auto-allow filesystem write operations. if ( toolPermissionContext.mode === 'acceptEdits' && - isFilesystemCommand(baseCmd) + isAcceptEditsWriteCommand(baseCmd) ) { // Guard: always run dangerous path check for rm/rmdir before auto-allowing. // This prevents rm -rf ~ / rm -rf / from bypassing checkDangerousRemovalPaths @@ -62,6 +114,37 @@ function validateCommandForMode( } } + // In Accept Edits mode, only auto-allow read-only commands if they still + // pass the full read-only validator. This prevents redirects and mutating + // find forms from being silently auto-approved. + if ( + toolPermissionContext.mode === 'acceptEdits' && + isAcceptEditsReadOnlyCommand(baseCmd) + ) { + if (hasShellRedirection(originalInput)) { + return { + behavior: 'passthrough', + message: + 'Read-only commands with shell redirection require normal permission checks', + } + } + + const readOnlyResult = checkReadOnlyConstraints( + { command: cmd } as z.infer, + false, + ) + if (readOnlyResult.behavior === 'allow') { + return { + behavior: 'allow', + updatedInput: { command: cmd }, + decisionReason: { + type: 'mode', + mode: 'acceptEdits', + }, + } + } + } + return { behavior: 'passthrough', message: `No mode-specific handling for '${baseCmd}' in ${toolPermissionContext.mode} mode`, @@ -106,7 +189,7 @@ export function checkPermissionMode( // Check each subcommand for (const cmd of commands) { - const result = validateCommandForMode(cmd, toolPermissionContext) + const result = validateCommandForMode(cmd, toolPermissionContext, input.command) // If any command triggers mode-specific behavior, return that result if (result.behavior !== 'passthrough') { @@ -124,5 +207,7 @@ export function checkPermissionMode( export function getAutoAllowedCommands( mode: ToolPermissionContext['mode'], ): readonly string[] { - return mode === 'acceptEdits' ? ACCEPT_EDITS_ALLOWED_COMMANDS : [] + return mode === 'acceptEdits' + ? [...ACCEPT_EDITS_WRITE_COMMANDS, ...ACCEPT_EDITS_READ_ONLY_COMMANDS] + : [] }