use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PermissionType {
Curl,
Cargo,
Git,
WorktreeFileOp,
Unknown,
}
pub const APPROVAL_MARKERS: &[&str] = &[
"requires approval",
"do you want to proceed",
"do you want to allow",
"(y/n)",
"[y/N]",
"Allow this command",
];
#[must_use]
pub fn capture_pane(session: &str, pane_index: usize) -> Option<String> {
let target = format!("{session}:0.{pane_index}");
let output = Command::new("tmux")
.args(["capture-pane", "-p", "-t", &target])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let s = String::from_utf8(output.stdout).ok()?;
Some(s)
}
#[must_use]
pub fn classify_capture(captured: &str) -> Option<PermissionType> {
if !APPROVAL_MARKERS.iter().any(|m| captured.contains(m)) {
return None;
}
Some(classify_command_class(captured))
}
fn classify_command_class(captured: &str) -> PermissionType {
if captured.contains("curl") {
PermissionType::Curl
} else if captured.contains("cargo fmt")
|| captured.contains("cargo clippy")
|| captured.contains("cargo test")
|| captured.contains("cargo build")
{
PermissionType::Cargo
} else if captured.contains("git commit") || captured.contains("git push") {
PermissionType::Git
} else {
PermissionType::Unknown
}
}
#[must_use]
pub fn detect_permission_prompt(session: &str, pane_index: usize) -> Option<PermissionType> {
let content = capture_pane(session, pane_index)?;
classify_capture(&content)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn captures_with_no_marker_classify_to_none() {
let captured = "lorem ipsum dolor sit amet\n$ ls\n";
assert_eq!(classify_capture(captured), None);
}
#[test]
fn curl_prompt_classifies_curl() {
let captured = "curl http://127.0.0.1:9119/publish\nrequires approval\n";
assert_eq!(classify_capture(captured), Some(PermissionType::Curl));
}
#[test]
fn cargo_test_prompt_classifies_cargo() {
let captured = "do you want to proceed\nRunning cargo test --workspace";
assert_eq!(classify_capture(captured), Some(PermissionType::Cargo));
}
#[test]
fn cargo_fmt_prompt_classifies_cargo() {
let captured = "[y/N] cargo fmt --all";
assert_eq!(classify_capture(captured), Some(PermissionType::Cargo));
}
#[test]
fn cargo_clippy_prompt_classifies_cargo() {
let captured = "Allow this command: cargo clippy";
assert_eq!(classify_capture(captured), Some(PermissionType::Cargo));
}
#[test]
fn cargo_build_prompt_classifies_cargo() {
let captured = "(y/n) cargo build --release";
assert_eq!(classify_capture(captured), Some(PermissionType::Cargo));
}
#[test]
fn git_commit_prompt_classifies_git() {
let captured = "git commit -m hi\nrequires approval";
assert_eq!(classify_capture(captured), Some(PermissionType::Git));
}
#[test]
fn git_push_prompt_classifies_git() {
let captured = "git push origin main\nrequires approval";
assert_eq!(classify_capture(captured), Some(PermissionType::Git));
}
#[test]
fn unrecognized_command_classifies_unknown() {
let captured = "rm -rf /tmp/foo\nrequires approval";
assert_eq!(classify_capture(captured), Some(PermissionType::Unknown));
}
#[test]
fn marker_alone_without_command_is_unknown() {
let captured = "requires approval";
assert_eq!(classify_capture(captured), Some(PermissionType::Unknown));
}
#[test]
fn capture_pane_returns_none_for_nonexistent_session() {
let out = capture_pane("paw-nonexistent-session-aabbccdd-zz", 0);
assert!(
out.is_none(),
"nonexistent session should not capture, got {out:?}"
);
}
#[test]
fn detect_permission_prompt_returns_none_for_nonexistent_session() {
let out = detect_permission_prompt("paw-nonexistent-session-aabbccdd-zz", 0);
assert_eq!(out, None);
}
#[test]
fn claude_y_n_requires_approval_marker_classifies_each_class() {
let curl_prompt =
"Bash command:\ncurl http://127.0.0.1:9119/publish\nrequires approval [y/N]";
assert_eq!(
classify_capture(curl_prompt),
Some(PermissionType::Curl),
"Claude curl prompt with `requires approval [y/N]` must classify Curl"
);
let cargo_prompt = "Bash command:\ncargo test --workspace\nrequires approval [y/N]";
assert_eq!(
classify_capture(cargo_prompt),
Some(PermissionType::Cargo),
"Claude cargo prompt with combined markers must classify Cargo"
);
let git_prompt = "Bash command:\ngit commit -m \"wip\"\nrequires approval [y/N]";
assert_eq!(
classify_capture(git_prompt),
Some(PermissionType::Git),
"Claude git prompt with combined markers must classify Git"
);
let unknown_prompt = "Bash command:\nrm -rf /tmp/foo\nrequires approval [y/N]";
assert_eq!(
classify_capture(unknown_prompt),
Some(PermissionType::Unknown),
"an unrecognised command must classify Unknown even with markers"
);
let no_marker = "rm -rf /tmp/foo\n$ ls\n";
assert_eq!(
classify_capture(no_marker),
None,
"absent any approval marker, classify_capture must return None"
);
}
}