Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/but-claude/src/claude_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub fn fmt_claude_settings() -> Result<String> {
);
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
Expand All @@ -31,6 +32,12 @@ pub fn fmt_claude_settings() -> Result<String> {
"type": "command",
"command": post_cmd
}]
}, {
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": bash_post_cmd
}]
}],
"Stop": [{
"matcher": "",
Expand Down
49 changes: 48 additions & 1 deletion crates/but-claude/src/hooks/mod.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -85,6 +87,51 @@ pub struct ClaudeStopInput {
pub stop_hook_active: Option<bool>,
}

#[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<String>,
pub run_in_background: Option<bool>,
pub timeout: Option<u64>,
}

#[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<ClaudeHookOutput> {
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<ClaudeHookOutput> {
let input: ClaudeStopInput = serde_json::from_str(&stdin()?)
.map_err(|e| anyhow::anyhow!("Failed to parse input JSON: {}", e))?;
Expand Down
34 changes: 34 additions & 0 deletions crates/but-claude/src/hooks/rm_file_matching.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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<RmMatcher> {
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!()
}
}
9 changes: 9 additions & 0 deletions crates/but/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions crates/but/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading