use anyhow::Context;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum HookEvent {
PreToolUse,
PostToolUse,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ToolHook {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
pub hook_event_name: HookEvent,
#[serde(flatten)]
pub tool: Tool,
}
#[derive(Debug)]
pub enum Hook {
Tool(ToolHook),
UserPrompt,
}
#[non_exhaustive]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "tool_name", content = "tool_input")]
pub enum Tool {
Read(FileToolInput),
Write(FileToolInput),
Edit(FileToolInput),
MultiEdit(FileToolInput),
Bash(BashToolInput),
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct FileToolInput {
pub file_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_string: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_string: Option<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct BashToolInput {
pub command: String,
pub description: String,
}
pub fn parse_hook(input: &str) -> anyhow::Result<Hook> {
let value: serde_json::Value = serde_json::from_str(input).context("Invalid JSON")?;
let event_name = value
.get("hook_event_name")
.and_then(|v| v.as_str())
.context("Missing hook_event_name")?;
match event_name {
"UserPromptSubmit" => Ok(Hook::UserPrompt),
"PreToolUse" | "PostToolUse" => {
let hook: ToolHook = serde_json::from_str(input).context("Invalid tool hook")?;
Ok(Hook::Tool(hook))
}
_ => {
anyhow::bail!("Invalid hook received")
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionDecision {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HookSpecificOutput {
pub hook_event_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_decision: Option<PermissionDecision>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_decision_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HookOutput {
#[serde(rename = "continue", skip_serializing_if = "Option::is_none")]
pub continue_execution: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suppress_output: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hook_specific_output: Option<HookSpecificOutput>,
}
impl HookOutput {
pub fn new() -> Self {
Self {
continue_execution: None,
stop_reason: None,
suppress_output: None,
system_message: None,
decision: None,
reason: None,
hook_specific_output: None,
}
}
#[allow(dead_code)]
pub fn with_continue(mut self, continue_execution: bool) -> Self {
self.continue_execution = Some(continue_execution);
self
}
#[allow(dead_code)]
pub fn with_stop_reason(mut self, reason: impl Into<String>) -> Self {
self.stop_reason = Some(reason.into());
self
}
#[allow(dead_code)]
pub fn with_suppress_output(mut self, suppress: bool) -> Self {
self.suppress_output = Some(suppress);
self
}
#[allow(dead_code)]
pub fn with_system_message(mut self, message: impl Into<String>) -> Self {
self.system_message = Some(message.into());
self
}
pub fn with_permission_decision(
mut self,
decision: PermissionDecision,
reason: Option<String>,
) -> Self {
self.hook_specific_output = Some(HookSpecificOutput {
hook_event_name: "PreToolUse".to_string(),
permission_decision: Some(decision),
permission_decision_reason: reason,
additional_context: None,
});
self
}
pub fn with_additional_context(mut self, context: impl Into<String>) -> Self {
self.hook_specific_output = Some(HookSpecificOutput {
hook_event_name: "UserPromptSubmit".to_string(),
permission_decision: None,
permission_decision_reason: None,
additional_context: Some(context.into()),
});
self
}
pub fn to_json(&self) -> anyhow::Result<String> {
serde_json::to_string(self).context("Failed to serialize HookOutput")
}
#[allow(dead_code)]
pub fn to_json_pretty(&self) -> anyhow::Result<String> {
serde_json::to_string_pretty(self).context("Failed to serialize HookOutput")
}
}
impl Default for HookOutput {
fn default() -> Self {
Self::new()
}
}