Skip to main content

claude_agent/
hooks.rs

1// Layer 2: THE SKULL — Lifecycle Hooks
2// 5 hinged panels: SESSION START, PRE-BASH GUARD, POST-EDIT LINT, POST-FAIL DIAGNOSE, AUTO-HANDOFF
3
4use crate::types::*;
5
6pub struct HookSystem {
7    hooks: Vec<(HookEvent, String)>,
8}
9
10impl Default for HookSystem {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15
16impl HookSystem {
17    pub fn new() -> Self {
18        Self { hooks: Vec::new() }
19    }
20
21    pub fn register_defaults(&mut self) {
22        self.hooks.push((HookEvent::SessionStart, "session-start".into()));
23        self.hooks.push((HookEvent::PreToolUse, "pre-bash-guard".into()));
24        self.hooks.push((HookEvent::PostToolUse, "post-edit-lint".into()));
25        self.hooks.push((HookEvent::PostToolFailure, "post-fail-diagnose".into()));
26        self.hooks.push((HookEvent::SessionStop, "auto-handoff".into()));
27    }
28
29    pub fn fire(&self, event: HookEvent, tool_name: Option<&str>, input: Option<&serde_json::Value>) -> HookResult {
30        // Pre-bash guard: block dangerous commands
31        if event == HookEvent::PreToolUse && tool_name == Some("bash") {
32            if let Some(input) = input {
33                let command = input.get("command").and_then(|v| v.as_str()).unwrap_or("");
34                let dangerous = [
35                    "rm -rf /", "rm -rf ~", "> /dev/sd", "mkfs.", "chmod 000",
36                ];
37                for pattern in dangerous {
38                    if command.contains(pattern) {
39                        return HookResult {
40                            allow: false,
41                            message: Some(format!("Dangerous command blocked: {command}")),
42                        };
43                    }
44                }
45            }
46        }
47
48        HookResult { allow: true, message: None }
49    }
50
51}