Skip to main content

polyhook_core/
detect.rs

1use crate::types::CallerKind;
2
3/// Detect which agent is calling the hook.
4///
5/// Priority:
6/// 1. `POLYHOOK_CALLER` env var (explicit override)
7/// 2. Agent-specific env vars
8/// 3. Heuristics on the raw stdin JSON shape
9/// 4. `Unknown`
10pub fn detect_caller(stdin: &serde_json::Value) -> CallerKind {
11    // 1. Explicit override via env var
12    if let Ok(val) = std::env::var("POLYHOOK_CALLER") {
13        match val.to_lowercase().as_str() {
14            "claude-code" | "claudecode" => return CallerKind::ClaudeCode,
15            "cursor" => return CallerKind::Cursor,
16            "windsurf" => return CallerKind::Windsurf,
17            "cline" => return CallerKind::Cline,
18            "amp" => return CallerKind::Amp,
19            "gemini-cli" | "geminicli" => return CallerKind::GeminiCli,
20            "hermes" | "hermes-agent" | "hermesagent" => return CallerKind::Hermes,
21            "pi" => return CallerKind::Pi,
22            "codex" => return CallerKind::Codex,
23            _ => {}
24        }
25    }
26
27    // 2. Agent-specific env vars
28    if std::env::var("CLAUDE_CODE_VERSION").is_ok() {
29        return CallerKind::ClaudeCode;
30    }
31    if std::env::var("CURSOR_SESSION_ID").is_ok() {
32        return CallerKind::Cursor;
33    }
34    if std::env::var("WINDSURF_SESSION_ID").is_ok() {
35        return CallerKind::Windsurf;
36    }
37    if std::env::var("CLINE_SESSION_ID").is_ok() {
38        return CallerKind::Cline;
39    }
40    if std::env::var("AMP_SESSION_ID").is_ok() {
41        return CallerKind::Amp;
42    }
43    if std::env::var("GEMINI_PROJECT_DIR").is_ok() {
44        return CallerKind::GeminiCli;
45    }
46
47    // 3. JSON shape heuristics
48    if let Some(obj) = stdin.as_object() {
49        let has = |key: &str| obj.contains_key(key);
50        let str_val = |key: &str| obj.get(key).and_then(|v| v.as_str()).unwrap_or("");
51
52        // Gemini CLI / Hermes: hook_event_name with caller-specific values.
53        // Checked before the Claude Code heuristic because all three send
54        // tool_name + tool_input for tool events.
55        match str_val("hook_event_name") {
56            "BeforeTool"
57            | "AfterTool"
58            | "BeforeAgent"
59            | "AfterAgent"
60            | "BeforeModel"
61            | "AfterModel"
62            | "BeforeToolSelection"
63            | "PreCompress"
64            | "SessionStart"
65            | "SessionEnd" => return CallerKind::GeminiCli,
66            "pre_tool_call"
67            | "post_tool_call"
68            | "pre_llm_call"
69            | "on_session_start"
70            | "on_session_end"
71            | "on_session_finalize"
72            | "subagent_stop" => {
73                return CallerKind::Hermes;
74            }
75            _ => {}
76        }
77
78        // Cline's file hooks name the event in `hookName` and tag every
79        // payload with `clineVersion`.
80        if has("hookName") && has("clineVersion") {
81            return CallerKind::Cline;
82        }
83
84        // Codex speaks Claude Code's hook format but adds a per-turn
85        // `turn_id` to every payload, which Claude Code never sends.
86        if has("hook_event_name") && has("turn_id") {
87            return CallerKind::Codex;
88        }
89
90        // Claude Code prompt events carry the prompt, not tool fields.
91        if str_val("hook_event_name") == "UserPromptSubmit" {
92            return CallerKind::ClaudeCode;
93        }
94
95        if has("tool_name") && has("tool_input") {
96            return CallerKind::ClaudeCode;
97        }
98        if has("type") && has("toolCall") {
99            return CallerKind::Cursor;
100        }
101        if has("event") && has("parameters") {
102            return CallerKind::Windsurf;
103        }
104        // Cline uses toolName (not toolCall)
105        if has("type") && has("toolName") && !has("toolCall") {
106            return CallerKind::Cline;
107        }
108        if has("kind") {
109            return CallerKind::Amp;
110        }
111    }
112
113    CallerKind::Unknown
114}
115
116// ---------------------------------------------------------------------------
117// Tests
118// ---------------------------------------------------------------------------
119
120#[cfg(test)]
121#[path = "detect_tests.rs"]
122mod tests;