aboutsummaryrefslogtreecommitdiff
path: root/src/detect
diff options
context:
space:
mode:
authorcaner-akca <94343893+caner-akca@users.noreply.github.com>2026-09-07 02:27:04 +0300
committerGitHub <noreply@github.com>2026-09-07 02:27:04 +0300
commit4b5e9bda239a0b6903889062d756424578e94691 (patch)
tree95f44b7e4b675683edcc9a093e048ec1c10d656e /src/detect
parentc7b79294e28fe7c835691a25597fabf226ecfc20 (diff)
fix: match claude bash permission prompts at every cursor position (#3615)
Claude marks the selected option with "❯", but bash_permission_prompt did not allow that prefix on numbered branches and did not cover the three-option "don't ask again" layout. Match both two- and three-option Bash approval prompts regardless of the selected option. refs #2650
Diffstat (limited to 'src/detect')
-rw-r--r--src/detect/manifest/tests.rs102
-rw-r--r--src/detect/manifests/claude.toml18
2 files changed, 117 insertions, 3 deletions
diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs
index 00730a44..e1073f3c 100644
--- a/src/detect/manifest/tests.rs
+++ b/src/detect/manifest/tests.rs
@@ -739,6 +739,108 @@ fn claude_blocker_with_background_shell_remains_blocked() {
}
#[test]
+fn claude_bash_prompt_with_dont_ask_again_option_matches_bash_rule() {
+ // Captured from a Bash approval prompt at its resting cursor position. The
+ // "don't ask again" choice pushes No to option 3, so the only cursor-free
+ // option line is one bash_permission_prompt used not to cover, which let
+ // the narrower generic_permission_prompt claim the prompt instead (#2650).
+ let screen = concat!(
+ "────────────────────────────────────────────────────────────────
+",
+ " Bash command
+
+",
+ " curl -sS -o /tmp/probe.html https://example.com
+",
+ " Download example.com to /tmp/probe.html
+
+",
+ " This command requires approval
+
+",
+ " Do you want to proceed?
+",
+ " ❯ 1. Yes
+",
+ " 2. Yes, and don't ask again for: curl *
+",
+ " 3. No
+
+",
+ " Esc to cancel · Tab to amend · ctrl+e to explain
+",
+ );
+ let result = osc_explain(Agent::Claude, screen, "", "");
+
+ assert_eq!(result.state, AgentState::Blocked);
+ assert_eq!(
+ result.matched_rule.as_ref().map(|rule| rule.id.as_str()),
+ Some("bash_permission_prompt")
+ );
+ assert!(result.visible_blocker);
+}
+
+#[test]
+fn claude_permission_prompt_matches_at_every_cursor_position() {
+ // The selected option carries "❯", so no option branch may assume its line
+ // is cursor-free. Walk the cursor across both option layouts.
+ let layouts: [&[&str]; 2] = [
+ &[" ❯ 1. Yes", " 2. No"],
+ &[
+ " ❯ 1. Yes",
+ " 2. Yes, and don't ask again for: curl *",
+ " 3. No",
+ ],
+ ];
+
+ for layout in layouts {
+ for selected in 0..layout.len() {
+ let options: Vec<String> = layout
+ .iter()
+ .enumerate()
+ .map(|(index, line)| {
+ let bare = line.trim_start().trim_start_matches('❯').trim_start();
+ if index == selected {
+ format!(" ❯ {bare}")
+ } else {
+ format!(" {bare}")
+ }
+ })
+ .collect();
+ let screen = format!(
+ concat!(
+ "────────────────────────────────────────────────────────────────
+",
+ " Bash command
+
+",
+ " curl -sS https://example.com
+
+",
+ " Do you want to proceed?
+",
+ "{}
+
+",
+ " Esc to cancel · Tab to amend · ctrl+e to explain
+",
+ ),
+ options.join("\n"),
+ );
+ let result = osc_explain(Agent::Claude, &screen, "", "");
+
+ assert_eq!(
+ result.matched_rule.as_ref().map(|rule| rule.id.as_str()),
+ Some("bash_permission_prompt"),
+ "{options:?} selected={selected}"
+ );
+ assert_eq!(result.state, AgentState::Blocked, "selected={selected}");
+ assert!(result.visible_blocker, "selected={selected}");
+ }
+ }
+}
+
+#[test]
fn claude_osc_title_braille_prefix_is_working() {
// "⠂" is U+2802, in the braille block U+2800-U+28FF
let result = osc_explain(Agent::Claude, "", "⠂ project", "");
diff --git a/src/detect/manifests/claude.toml b/src/detect/manifests/claude.toml
index 2a26a334..16482764 100644
--- a/src/detect/manifests/claude.toml
+++ b/src/detect/manifests/claude.toml
@@ -1,7 +1,7 @@
id = "claude"
-version = "2026.08.31.1"
+version = "2026.09.04.1"
min_engine_version = 2
-updated_at = "2026-08-31T00:00:00Z"
+updated_at = "2026-09-04T00:00:00Z"
aliases = ["claude-code"]
[[rules]]
@@ -162,8 +162,20 @@ any = [
{ contains = ["tab to amend"] },
{ contains = ["ctrl+e to explain"] },
]
+# Claude marks the selected option with "❯", so every option branch has to allow
+# that prefix. Numbered branches that omit it only match options the cursor has
+# moved away from, which left the resting layout below unmatched here (#2650):
+# ❯ 1. Yes / 2. Yes, and don't ask again for: <cmd> / 3. No
+# Cover both the two-option and "don't ask again" three-option shapes so this
+# rule, not the narrower generic_permission_prompt, claims Bash approvals.
all = [
- { any = [{ line_regex = ['(?i)^\s*❯?\s*yes\b'] }, { line_regex = ['(?i)^\s*1\.\s*yes\b'] }, { line_regex = ['(?i)^\s*2\.\s*no\b'] }] },
+ { any = [
+ { line_regex = ['(?i)^\s*❯?\s*yes\b'] },
+ { line_regex = ['(?i)^\s*❯?\s*1\.\s*yes\b'] },
+ { line_regex = ['(?i)^\s*❯?\s*2\.\s*yes\b'] },
+ { line_regex = ['(?i)^\s*❯?\s*2\.\s*no\b'] },
+ { line_regex = ['(?i)^\s*❯?\s*3\.\s*no\b'] },
+ ] },
]
[[rules]]