Bug: Command substitution denial is inconsistently applied and opaque
What happened?
When the agent runs a shell command containing command substitution ($(), backticks, <(), >()), the security check that should deny such commands is inconsistently applied:
- In some cases (compound commands where one sub-command matches an existing allow rule), the command is hard-denied with an opaque error message that does not mention command substitution. The user cannot override this even in YOLO mode, and the agent cannot understand why the command was rejected.
- In other cases (simple commands with no relevant permission rules), the command substitution check is entirely bypassed — the command is treated like any other non-read-only command and the user can approve it without any security warning.
What did you expect to happen?
The command substitution security check should be applied consistently — either always deny (with a clear reason), or always ask the user for confirmation (with a clear warning about command substitution). The current behavior where the check is sometimes applied and sometimes bypassed based on whether the user has configured permission rules is clearly unintended.
Additionally, when a command is denied due to command substitution, the error message should clearly state the reason, so both the user and the agent can understand and adapt.
Reproduction scenario
The 'deny' behavior is triggered when the PermissionManager has "relevant" rules for the command (i.e., some rules could potentially apply to the command context), but no rule actually matches the specific command containing command substitution. This most commonly happens with compound commands where one sub-command matches an existing allow rule (making the rules "relevant"), while another sub-command contains command substitution but doesn't match any rule.
Test command (in YOLO mode):
echo hello && python3 -c "print($(echo hello))"
Why this triggers the deny:
- The compound command is split into sub-commands:
echo hello and python3 -c "print($(echo hello))"
echo hello matches an existing Bash(echo *) allow rule → the PermissionManager considers its rules "relevant" to this command context
python3 -c "print($(echo hello))" doesn't match any rule → evaluateSingle() returns 'default'
- For
'default' results on shell commands, resolveDefaultPermission() is called, which detects command substitution and returns 'deny'
- The most restrictive result (
'deny') wins over 'allow' → the entire compound command is denied
What the agent sees (returned as tool error):
Tool "run_shell_command" is denied by permission rules.
What the user sees (rendered in the TUI):
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ x Shell {"command":"echo hello && python3 -c \"print($(echo hello))\"","description":"Test compound command with substitution in YOLO mode"} │
│ │
│ Tool "run_shell_command" is denied by permission rules. │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Note: a simple command like python3 -c "print($(echo hello))" alone would NOT trigger the deny — it would get 'ask' (because the PermissionManager has no relevant rules for python3 and skips evaluation entirely). This inconsistency makes the behavior especially confusing.
Anything else we need to know?
Root cause analysis:
The inconsistency comes from the L3→L4 permission evaluation flow:
- L3 (
ShellToolInvocation.getDefaultPermission()): Does NOT check for command substitution. AST analysis marks commands with substitution as non-read-only → returns 'ask'.
- L4 (
PermissionManager.evaluate()): When hasRelevantRules() returns true (some rule could apply), evaluate() is called. For sub-commands where no rule matches ('default'), resolveDefaultPermission() detects command substitution → 'deny'. But when hasRelevantRules() returns false (no rules could apply at all), PM evaluation is skipped entirely and the command keeps its L3 'ask' — the command substitution check is never reached.
- L5: YOLO mode auto-approves
'ask' but does NOT override 'deny'.
This means the command substitution security check only fires when the PermissionManager is invoked (hasRelevantRules=true), but is completely bypassed when no rules are relevant. The hasRelevantRules() gate was likely intended as a performance optimization, but it inadvertently creates a security inconsistency.
Suggested fix:
- Change
resolveDefaultPermission() to return 'ask' instead of 'deny' for command substitution, ensuring consistent behavior regardless of the user's rule configuration.
- Add a specific warning about command substitution in the confirmation prompt, so users can make informed decisions.
- Include the denial reason in the error message when a command substitution command is denied, so the agent can understand and adapt.
中文
发生了什么?
当 Agent 执行包含命令替换($()、反引号、<()、>())的 shell 命令时,应该拒绝此类命令的安全检查不一致地执行:
- 某些情况下(复合命令中一个子命令匹配已有的 allow 规则),命令被硬拒绝,错误消息不透明且不提及命令替换。用户即使在 YOLO 模式下也无法覆盖,Agent 也无法理解为何被拒绝。
- 其他情况下(没有相关权限规则的简单命令),命令替换检查被完全绕过——命令被当作普通的非只读命令处理,用户可以在无安全警告的情况下批准。
你期望发生什么?
命令替换安全检查应该一致地执行——要么始终拒绝(附带明确原因),要么始终要求用户确认(附带命令替换安全警告)。当前行为中,检查有时执行有时绕过,取决于用户是否配置了权限规则,这显然不是设计意图。
此外,当命令因命令替换被拒绝时,错误消息应明确说明原因,让用户和 Agent 都能理解并调整。
复现场景
'deny' 行为在 PermissionManager 有"相关"规则(即某些规则可能适用于该命令上下文),但没有规则实际匹配包含命令替换的特定命令时触发。这最常见于复合命令——其中一个子命令匹配已有的 allow 规则(使规则变得"相关"),另一个子命令包含命令替换但不匹配任何规则。
测试命令(YOLO 模式下):
echo hello && python3 -c "print($(echo hello))"
触发原因:
- 复合命令被拆分为子命令:
echo hello 和 python3 -c "print($(echo hello))"
echo hello 匹配已有的 Bash(echo *) allow 规则 → PermissionManager 认为它的规则与此命令上下文"相关"
python3 -c "print($(echo hello))" 不匹配任何规则 → evaluateSingle() 返回 'default'
- 对于 shell 命令的
'default' 结果,调用 resolveDefaultPermission(),检测到命令替换并返回 'deny'
- 最严格的结果(
'deny')胜过 'allow' → 整个复合命令被拒绝
Agent 看到的内容(作为工具错误返回):
Tool "run_shell_command" is denied by permission rules.
用户看到的内容(TUI 渲染):
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ x Shell {"command":"echo hello && python3 -c \"print($(echo hello))\"","description":"Test compound command with substitution in YOLO mode"} │
│ │
│ Tool "run_shell_command" is denied by permission rules. │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
注意:单独运行 python3 -c "print($(echo hello))" 不会触发拒绝——它会得到 'ask'(因为 PermissionManager 对 python3 没有相关规则,会跳过评估)。这种不一致性使行为特别令人困惑。
补充信息
根本原因分析:
不一致性来自 L3→L4 权限评估流程:
- L3(
ShellToolInvocation.getDefaultPermission()):不检查命令替换。AST 分析将包含替换的命令标记为非只读 → 返回 'ask'。
- L4(
PermissionManager.evaluate()):当 hasRelevantRules() 返回 true(某些规则可能适用)时,调用 evaluate()。对于没有规则匹配的子命令('default'),resolveDefaultPermission() 检测命令替换 → 'deny'。但当 hasRelevantRules() 返回 false(没有任何规则可能适用)时,PM 评估被完全跳过,命令保持 L3 的 'ask'——命令替换检查永远不会被执行。
- L5:YOLO 模式自动批准
'ask',但不覆盖 'deny'。
这意味着命令替换安全检查只在 PermissionManager 被调用(hasRelevantRules=true)时触发,而在没有相关规则时被完全绕过。hasRelevantRules() 门控可能原本是性能优化,但它无意中造成了安全不一致。
建议修复:
- 将
resolveDefaultPermission() 对命令替换的返回值从 'deny' 改为 'ask',确保无论用户的规则配置如何,行为都一致。
- 在确认提示中添加关于命令替换的特定警告,让用户做出知情决策。
- 在命令替换命令被拒绝时,在错误消息中包含拒绝原因,让 Agent 能理解并调整。
Bug: Command substitution denial is inconsistently applied and opaque
What happened?
When the agent runs a shell command containing command substitution (
$(), backticks,<(),>()), the security check that should deny such commands is inconsistently applied:What did you expect to happen?
The command substitution security check should be applied consistently — either always deny (with a clear reason), or always ask the user for confirmation (with a clear warning about command substitution). The current behavior where the check is sometimes applied and sometimes bypassed based on whether the user has configured permission rules is clearly unintended.
Additionally, when a command is denied due to command substitution, the error message should clearly state the reason, so both the user and the agent can understand and adapt.
Reproduction scenario
The
'deny'behavior is triggered when the PermissionManager has "relevant" rules for the command (i.e., some rules could potentially apply to the command context), but no rule actually matches the specific command containing command substitution. This most commonly happens with compound commands where one sub-command matches an existing allow rule (making the rules "relevant"), while another sub-command contains command substitution but doesn't match any rule.Test command (in YOLO mode):
Why this triggers the deny:
echo helloandpython3 -c "print($(echo hello))"echo hellomatches an existingBash(echo *)allow rule → the PermissionManager considers its rules "relevant" to this command contextpython3 -c "print($(echo hello))"doesn't match any rule →evaluateSingle()returns'default''default'results on shell commands,resolveDefaultPermission()is called, which detects command substitution and returns'deny''deny') wins over'allow'→ the entire compound command is deniedWhat the agent sees (returned as tool error):
What the user sees (rendered in the TUI):
Note: a simple command like
python3 -c "print($(echo hello))"alone would NOT trigger the deny — it would get'ask'(because the PermissionManager has no relevant rules forpython3and skips evaluation entirely). This inconsistency makes the behavior especially confusing.Anything else we need to know?
Root cause analysis:
The inconsistency comes from the L3→L4 permission evaluation flow:
ShellToolInvocation.getDefaultPermission()): Does NOT check for command substitution. AST analysis marks commands with substitution as non-read-only → returns'ask'.PermissionManager.evaluate()): WhenhasRelevantRules()returns true (some rule could apply),evaluate()is called. For sub-commands where no rule matches ('default'),resolveDefaultPermission()detects command substitution →'deny'. But whenhasRelevantRules()returns false (no rules could apply at all), PM evaluation is skipped entirely and the command keeps its L3'ask'— the command substitution check is never reached.'ask'but does NOT override'deny'.This means the command substitution security check only fires when the PermissionManager is invoked (hasRelevantRules=true), but is completely bypassed when no rules are relevant. The
hasRelevantRules()gate was likely intended as a performance optimization, but it inadvertently creates a security inconsistency.Suggested fix:
resolveDefaultPermission()to return'ask'instead of'deny'for command substitution, ensuring consistent behavior regardless of the user's rule configuration.中文
发生了什么?
当 Agent 执行包含命令替换(
$()、反引号、<()、>())的 shell 命令时,应该拒绝此类命令的安全检查不一致地执行:你期望发生什么?
命令替换安全检查应该一致地执行——要么始终拒绝(附带明确原因),要么始终要求用户确认(附带命令替换安全警告)。当前行为中,检查有时执行有时绕过,取决于用户是否配置了权限规则,这显然不是设计意图。
此外,当命令因命令替换被拒绝时,错误消息应明确说明原因,让用户和 Agent 都能理解并调整。
复现场景
'deny'行为在 PermissionManager 有"相关"规则(即某些规则可能适用于该命令上下文),但没有规则实际匹配包含命令替换的特定命令时触发。这最常见于复合命令——其中一个子命令匹配已有的 allow 规则(使规则变得"相关"),另一个子命令包含命令替换但不匹配任何规则。测试命令(YOLO 模式下):
触发原因:
echo hello和python3 -c "print($(echo hello))"echo hello匹配已有的Bash(echo *)allow 规则 → PermissionManager 认为它的规则与此命令上下文"相关"python3 -c "print($(echo hello))"不匹配任何规则 →evaluateSingle()返回'default''default'结果,调用resolveDefaultPermission(),检测到命令替换并返回'deny''deny')胜过'allow'→ 整个复合命令被拒绝Agent 看到的内容(作为工具错误返回):
用户看到的内容(TUI 渲染):
注意:单独运行
python3 -c "print($(echo hello))"不会触发拒绝——它会得到'ask'(因为 PermissionManager 对python3没有相关规则,会跳过评估)。这种不一致性使行为特别令人困惑。补充信息
根本原因分析:
不一致性来自 L3→L4 权限评估流程:
ShellToolInvocation.getDefaultPermission()):不检查命令替换。AST 分析将包含替换的命令标记为非只读 → 返回'ask'。PermissionManager.evaluate()):当hasRelevantRules()返回 true(某些规则可能适用)时,调用evaluate()。对于没有规则匹配的子命令('default'),resolveDefaultPermission()检测命令替换 →'deny'。但当hasRelevantRules()返回 false(没有任何规则可能适用)时,PM 评估被完全跳过,命令保持 L3 的'ask'——命令替换检查永远不会被执行。'ask',但不覆盖'deny'。这意味着命令替换安全检查只在 PermissionManager 被调用(hasRelevantRules=true)时触发,而在没有相关规则时被完全绕过。
hasRelevantRules()门控可能原本是性能优化,但它无意中造成了安全不一致。建议修复:
resolveDefaultPermission()对命令替换的返回值从'deny'改为'ask',确保无论用户的规则配置如何,行为都一致。