feat(ai-terminal): add shell command whitelist for auto-approval#16931
feat(ai-terminal): add shell command whitelist for auto-approval#16931
Conversation
Adds command-level analysis and pattern-based whitelist for the shellExecute tool, allowing users to auto-approve specific commands while requiring confirmation for others. Key features: - Pattern-based whitelist with word-boundary prefix matching - Commands with dangerous patterns ($() or backticks) are never auto-approved - Concatenated commands (&&, ||, ;, |) require ALL sub-commands to match - UI section in AI Configuration → Tools tab for managing patterns - Patterns persist via preferences Additional changes: - Introduces shouldAutoApprove hook on ToolRequest interface, allowing tools to control their own auto-approval logic without hardcoding tool IDs in the chat service. - Adds ShellCommandAnalyzer for parsing shell commands and detecting dangerous patterns like command substitution. - Adds ShellCommandWhitelistService for managing whitelist patterns with word-boundary prefix matching (e.g., "git log" matches "git log --oneline" but not "git logger" or "sudo git log"). - Empty or whitespace-only patterns are rejected to prevent accidental match-all configurations.
There was a problem hiding this comment.
Thanks for the PR. I can confirm that the whitelist works for me.
Besides some details for code location and parsing, the main drawback of this PR is that it does not touch the confirmation UI of the shell execution. If the user knows about this whitelisting possibility, then they don't really know anymore what the buttons in the UI are doing.
I think we need to fully customize the behavior in the UI and make it explicit, e.g. when the LLM generates git log --oneline this are all the possible actions we can take:
Deny
- Deny
- Deny with reason
- Always deny (
git log) for this workspace - Always deny (
git log) for user - Always deny (
git log *) for this workspace - Always deny (
git log *) for user
Approve
- Approve
- Approve and whitelist (
git log) for this workspace - Approve and whitelist (
git log) for user - Approve and whitelist (
git log *) for this workspace - Approve and whitelist (
git log *) for user
Claude Code supports all of these actions, however by default in the TUI they only expose
- Approve
- Approve and whitelist (
git log *) for this workspace - Deny [optional reason]
The other options can be reached by editing the claude settings files or using /permissions within Claude Code.
Because we have a graphical UI I think we could even try to offer all these options, via the dropdowns. By default we should highlight Deny, Deny with reason, Approve and Approve and whitelist (git log *) for this workspace.
Note if a local approve or deny list exists, then the global options have no effect anymore (unless we want to merge settings), so we should not offer them anymore.
Or we implement local <-> global merging, then I guess we would need to check all patterns registered local and global. First we should check whether a command is allowed/denied locally and only if there is no decision then we check for allowed/denied globally.
I would be fine with doing this UI adaptation in a follow up as long as you, I or someone else does it before the next release is done.
| } | ||
|
|
||
| export const TOOL_CONFIRMATION_PREFERENCE = 'ai-features.chat.toolConfirmation'; | ||
| export const SHELL_COMMAND_WHITELIST_PREFERENCE = 'ai-features.chat.shellCommandWhitelist'; |
There was a problem hiding this comment.
I think this preference should be placed in the same package as the terminal tool itself.
| <p className="ai-shell-whitelist-description"> | ||
| {nls.localize( | ||
| 'theia/ai/ide/toolsConfiguration/shellWhitelist/description', | ||
| 'Commands matching these patterns will be automatically allowed without confirmation. ' + |
There was a problem hiding this comment.
Commands matching these patterns will be automatically allowed without confirmation
I would prefer using the same syntax as Claude Code, e.g.
git log --> matches just git log
git log * --> matches all possible parameters of git log
If we align with Claude Code then it will be trivial to sync/copy/import their settings to/from Theia and users don't need to know two different syntaxes (besides the Bash()) part which I would not add here
Edit: Note that Claude Code also supports this weird git log: * syntax. So I guess at least for importing we would need to be able to handle that.
There was a problem hiding this comment.
from claude docs: The space before * matters: Bash(ls ) matches ls -la but not lsof, while Bash(ls) matches both. The legacy :* suffix syntax is equivalent to * but is deprecated.
There was a problem hiding this comment.
Yeah, they have weird syntax. But still ls will not match ls -a. So at least we should align on * usage.
| /** | ||
| * Pattern to detect dangerous command substitution: $( or backticks | ||
| */ | ||
| const DANGEROUS_PATTERN = /\$\(|`/; |
There was a problem hiding this comment.
I think that there are more patterns like process substitutions or expansions. Maybe we also need to handle subshells?
| export function containsDangerousPatterns(command: string): boolean { | ||
| return DANGEROUS_PATTERN.test(command); | ||
| } | ||
|
|
||
| /** | ||
| * Parses a shell command string into individual sub-commands by splitting | ||
| * on concatenation operators (&&, ||, ;, |). | ||
| * | ||
| * @param command The shell command to parse | ||
| * @returns Array of trimmed sub-commands with empty entries filtered out | ||
| */ | ||
| export function parseCommand(command: string): string[] { | ||
| return command | ||
| .split(COMMAND_SEPARATOR_PATTERN) | ||
| .map(cmd => cmd.trim()) | ||
| .filter(cmd => cmd.length > 0); | ||
| } |
There was a problem hiding this comment.
Would be nicer to make them part of an injectable class so they can be easily overwritten by adopters
| */ | ||
| const DANGEROUS_PATTERN = /\$\(|`/; | ||
|
|
||
| /** |
There was a problem hiding this comment.
In general we could differentiate between dangerous, complex and simple commands. The command separation and whitelist matching should only be done on simple ones, to make sure we don't let something bad through.
It's the same in Claude Code. When the LLM generates a correct, non-dangerous command but it's too complex then it does not match to whitelists and does not offer to add it to whitelist.
|
|
||
| protected handlePatternInputChange(value: string): void { | ||
| this.newPatternInput = value; | ||
| this.update(); |
There was a problem hiding this comment.
In Theia we usually use uncontrolled inputs to avoid updating the widget for every keystroke
| * Returns false if the command contains dangerous patterns or if the whitelist is empty. | ||
| * Returns true only if ALL sub-commands match at least one whitelist pattern. | ||
| */ | ||
| isCommandAllowed(command: string): boolean { |
There was a problem hiding this comment.
I think we should also support a blacklist of never allowed commands to avoid showing a UI for them and blocking the agent, e.g. never allow git push
What it does
Adds command-level analysis and pattern-based whitelist for the shellExecute tool, allowing users to auto-approve specific commands while requiring confirmation for others.
Key features:
Additional changes:
How to test
Set shellExecute to
Confirm. Then add 2 commands that should be auto-approved. then ask the shell execute to execute those. alone, combined and combined with not approved tools.Follow-ups
Breaking changes
Attribution
Review checklist
nlsservice (for details, please see the Internationalization/Localization section in the Coding Guidelines)Reminder for reviewers