diff --git a/crates/but-claude/src/claude_config.rs b/crates/but-claude/src/claude_config.rs index 4b215b4877..e3f1c02ecf 100644 --- a/crates/but-claude/src/claude_config.rs +++ b/crates/but-claude/src/claude_config.rs @@ -12,6 +12,7 @@ pub fn fmt_claude_settings() -> Result { ); let pre_cmd = format!("{cli_cmd} claude pre-tool"); let post_cmd = format!("{cli_cmd} claude post-tool"); + let bash_post_cmd = format!("{cli_cmd} claude bash-post-tool"); let stop_cmd = format!("{cli_cmd} claude stop"); // We could just do string formatting, but this ensures that we've at least @@ -31,6 +32,12 @@ pub fn fmt_claude_settings() -> Result { "type": "command", "command": post_cmd }] + }, { + "matcher": "Bash", + "hooks": [{ + "type": "command", + "command": bash_post_cmd + }] }], "Stop": [{ "matcher": "", diff --git a/crates/but-claude/src/hooks/mod.rs b/crates/but-claude/src/hooks/mod.rs index ea2ec228f3..287054f1b6 100644 --- a/crates/but-claude/src/hooks/mod.rs +++ b/crates/but-claude/src/hooks/mod.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; -use std::io::{self, Read}; +use std::fs::OpenOptions; +use std::io::{self, Read, Write}; use std::path::Path; use std::str::FromStr; @@ -20,6 +21,7 @@ use serde::{Deserialize, Serialize}; // use crate::command::file_lock; mod file_lock; +mod rm_file_matching; use crate::claude_transcript::Transcript; use uuid::Uuid; @@ -85,6 +87,51 @@ pub struct ClaudeStopInput { pub stop_hook_active: Option, } +#[derive(Debug, Serialize, Deserialize)] +pub struct ClaudeBashPostToolUseInput { + pub session_id: String, + pub transcript_path: String, + pub hook_event_name: String, + pub tool_name: String, + pub tool_input: BashToolInput, + pub tool_response: BashToolResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BashToolInput { + pub command: String, + pub description: Option, + pub run_in_background: Option, + pub timeout: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BashToolResponse { + pub stdout: String, + pub stderr: String, + pub exit_code: i32, + pub command: String, +} + +pub fn handle_bash_post_tool_call() -> anyhow::Result { + let input: serde_json::Value = serde_json::from_str(&stdin()?) + .map_err(|e| anyhow::anyhow!("Failed to parse input JSON: {}", e))?; + + let mut file = OpenOptions::new() + .read(true) + .append(true) + .open("/tmp/hook-log")?; + writeln!(&mut file, "{:#?}", input)?; + + // For now, we'll just return a success response + // This can be extended to handle bash-specific logic in the future + Ok(ClaudeHookOutput { + do_continue: true, + stop_reason: String::default(), + suppress_output: true, + }) +} + pub async fn handle_stop() -> anyhow::Result { let input: ClaudeStopInput = serde_json::from_str(&stdin()?) .map_err(|e| anyhow::anyhow!("Failed to parse input JSON: {}", e))?; diff --git a/crates/but-claude/src/hooks/rm_file_matching.rs b/crates/but-claude/src/hooks/rm_file_matching.rs new file mode 100644 index 0000000000..32e4417353 --- /dev/null +++ b/crates/but-claude/src/hooks/rm_file_matching.rs @@ -0,0 +1,34 @@ +use std::path::Path; + +/// Matches a path that _could_ have been affected by the RM. +pub struct RmMatcher { + cwd: String, + patterns: Vec, +} + +impl RmMatcher { + /// Takes an RM command like: + /// - `rm -r foo/bar /tmp/asdf/**/bar/*` + /// - `rm "/foo/bar baz" + fn create(command: &str, cwd: &str) -> anyhow::Result { + let paths = command + .split(" ") + .skip(1) + .filter(|arg| !arg.starts_with("-")); + + let recursive = command + .split(" ") + .find(|arg| arg.starts_with("-") && arg.contains("r")); + + let patterns = paths.map(|path| { + let path = Path::new(path); + if path.is_absolute() { + path.to_string_lossy().to_string() + } else { + Path::new(cwd).join(path).to_string_lossy().to_string() + } + }); + + todo!() + } +} diff --git a/crates/but/src/args.rs b/crates/but/src/args.rs index 94d3903081..b1eff69054 100644 --- a/crates/but/src/args.rs +++ b/crates/but/src/args.rs @@ -92,6 +92,13 @@ pub enum CommandName { alias = "ClaudePostTool" )] ClaudePostTool, + #[clap( + alias = "claude-bash-post-tool", + alias = "claudebashposttool", + alias = "claudeBashPostTool", + alias = "ClaudeBashPostTool" + )] + ClaudeBashPostTool, #[clap( alias = "claude-stop", alias = "claudestop", @@ -141,6 +148,8 @@ pub mod claude { PreTool, #[clap(alias = "post-tool-use")] PostTool, + #[clap(alias = "bash-post-tool-use")] + BashPostTool, Stop, #[clap(alias = "pp")] PermissionPromptMcp, diff --git a/crates/but/src/main.rs b/crates/but/src/main.rs index 0ee15d50b8..a51fd2d852 100644 --- a/crates/but/src/main.rs +++ b/crates/but/src/main.rs @@ -69,6 +69,13 @@ async fn main() -> Result<()> { metrics_if_configured(app_settings, CommandName::ClaudePostTool, p).ok(); Ok(()) } + claude::Subcommands::BashPostTool => { + let result = but_claude::hooks::handle_bash_post_tool_call(); + let p = props(start, &result); + result.out_json(); + metrics_if_configured(app_settings, CommandName::ClaudeBashPostTool, p).ok(); + Ok(()) + } claude::Subcommands::Stop => { let result = but_claude::hooks::handle_stop().await; let p = props(start, &result);