Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d32e69f
fix(auto-approval): extract nested shell substitutions for allowlist
DaubnerF Oct 15, 2026
4001662
fix(auto-approval): close CodeRabbit review findings in command parser
DaubnerF Oct 17, 2026
7316cc7
fix(auto-approval): inspect arithmetic interiors and fix heredoc deli…
DaubnerF Oct 17, 2026
076ebee
fix(auto-approval): heredoc terminator join order and CR-bounded line…
DaubnerF Oct 17, 2026
c69a96b
fix(auto-approval): fail closed on CR-touched heredocs (#1947)
DaubnerF Oct 18, 2026
a199b50
Merge remote-tracking branch 'upstream/main' into fix/1947-substituti…
DaubnerF Oct 18, 2026
bc00ba2
fix(auto-approval): fail closed on CR in no-delimiter heredocs (#1947)
DaubnerF Oct 18, 2026
c44c4d3
perf: bound heredoc CR scan to the masked span
DaubnerF Oct 18, 2026
c2cd08a
fix: close heredoc fail-open and relax depth-0 closers
DaubnerF Oct 20, 2026
d5bd1bf
test: pin exact parser decisions and correct comment claims
DaubnerF Oct 21, 2026
9886548
fix: close heredoc opener-remainder fail-open inside substitutions
DaubnerF Oct 22, 2026
d71c234
Merge remote-tracking branch 'upstream/main' into fix/1947-substituti…
DaubnerF Oct 23, 2026
61bf7b9
test: re-pin procsub closer placeholder for shell-quote 1.12 tokeniza…
DaubnerF Oct 23, 2026
cdac899
fix: run $(( ... ) ... ) groups through the command-substitution path
DaubnerF Oct 23, 2026
c9cbad6
fix: close heredoc opener-line fail-opens and the fallback fail-open
DaubnerF Oct 26, 2026
266aa42
test: pin allow and deny decisions for the hidden heredoc substitution
DaubnerF Oct 26, 2026
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
563 changes: 563 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