Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
527 changes: 527 additions & 0 deletions src/core/auto-approval/__tests__/commands.spec.ts

Large diffs are not rendered by default.

41 changes: 32 additions & 9 deletions src/core/auto-approval/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,18 @@ export function findLongestPrefixMatch(command: string, prefixes: string[]): str
}

/**
* Command approval decision types
* Command approval decision types.
*
* `malformed_command` is the fail-closed outcome. The parser cannot fully
* account for the command's structure. The parser reports it for an
* unterminated quote or heredoc. It also reports it for an expansion
* delimiter ($(), ${}, backtick, <( or >() or an arithmetic expression
* ($(( or $[) that never closes. A stray "}" with no matching "{", a raw
* NUL byte, and substitution nesting deeper than the re-scan depth cap
* also produce this outcome. No approval may use a partial decomposition
* of the command.
* A shell never parses delimiters inside quoted regions, so unbalanced
* delimiters there do not qualify.
*/
export type CommandDecision = "auto_approve" | "auto_deny" | "ask_user" | "malformed_command"

Expand All @@ -136,7 +147,15 @@ export type CommandDecision = "auto_approve" | "auto_deny" | "ask_user" | "malfo
* - `"auto_approve"`: All sub-commands are explicitly allowed and no dangerous patterns detected
* - `"auto_deny"`: At least one sub-command is explicitly denied
* - `"ask_user"`: Mixed or no matches found, requires user decision, or contains dangerous patterns
* - `"malformed_command"`: Command contains an unterminated quote -- a shell syntax error that must not be auto-approved
* - `"malformed_command"`: The parser cannot fully account for the command's
* structure. Causes: an unterminated quote or heredoc, an expansion
* delimiter ($(...), ${...}, backtick, <( or >()) or arithmetic expression
* ($(( or $[) that never closes, a stray "}" with no matching "{", a raw
* NUL byte, or substitution nesting deeper than the parser's re-scan depth
* cap. The parser must not approve a command whose structure is unproven
* based on a partial decomposition. A shell never parses delimiter
* characters inside quoted regions, so unbalanced delimiters there stay
* parseable.
*
* **Examples:**
* ```typescript
Expand Down Expand Up @@ -214,15 +233,19 @@ export function getCommandDecisionDetailed(
}

// Parse into sub-commands (split by &&, ||, ;, |). parseCommand also
// detects shell syntax errors (unterminated quotes, unclosed heredocs) and
// returns a non-null parseError in that case.
// detects input it cannot account for. That covers unterminated quotes
// or heredocs, expansion delimiters ($(), ${}, backticks, <( or >() or
// arithmetic expressions ($(( or $[) that never close, a stray closer,
// a raw NUL byte, and substitution nesting that is too deep. In that
// case parseCommand returns a non-null parseError.
const { commands: subCommands, parseError } = parseCommand(command)

// Reject commands with a shell syntax error. An unterminated quote means
// the shell would report a parse error; in a compound command it may
// partially execute the well-formed prefix before aborting. Returning a
// distinct decision lets callers surface a useful message to the agent
// rather than silently presenting the command for user approval.
// Reject commands whose structure the parser cannot fully account for.
// The shell reports a parse error for such input. In a compound command
// the shell may partially execute the well-formed prefix before it
// stops. A distinct decision lets callers show the agent a useful
// message. It also avoids presenting the command silently for user
// approval.
if (parseError !== null) {
return { decision: "malformed_command", offendingCommand: command, parseError: parseError.message }
}
Expand Down
5 changes: 0 additions & 5 deletions src/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1604,11 +1604,6 @@
"count": 1
}
},
"shared/parse-command.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"shared/support-prompt.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 5
Expand Down
Loading
Loading