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            _ => {}
22        }
23    }
24
25    // 2. Agent-specific env vars
26    if std::env::var("CLAUDE_CODE_VERSION").is_ok() {
27        return CallerKind::ClaudeCode;
28    }
29    if std::env::var("CURSOR_SESSION_ID").is_ok() {
30        return CallerKind::Cursor;
31    }
32    if std::env::var("WINDSURF_SESSION_ID").is_ok() {
33        return CallerKind::Windsurf;
34    }
35    if std::env::var("CLINE_SESSION_ID").is_ok() {
36        return CallerKind::Cline;
37    }
38    if std::env::var("AMP_SESSION_ID").is_ok() {
39        return CallerKind::Amp;
40    }
41    if std::env::var("GEMINI_PROJECT_DIR").is_ok() {
42        return CallerKind::GeminiCli;
43    }
44
45    // 3. JSON shape heuristics
46    if let Some(obj) = stdin.as_object() {
47        let has = |key: &str| obj.contains_key(key);
48        let str_val = |key: &str| obj.get(key).and_then(|v| v.as_str()).unwrap_or("");
49
50        // Gemini CLI / Hermes: hook_event_name with caller-specific values.
51        // Checked before the Claude Code heuristic because all three send
52        // tool_name + tool_input for tool events.
53        match str_val("hook_event_name") {
54            "BeforeTool"
55            | "AfterTool"
56            | "BeforeAgent"
57            | "AfterAgent"
58            | "BeforeModel"
59            | "AfterModel"
60            | "BeforeToolSelection"
61            | "PreCompress"
62            | "SessionStart"
63            | "SessionEnd" => return CallerKind::GeminiCli,
64            "pre_tool_call"
65            | "post_tool_call"
66            | "pre_llm_call"
67            | "on_session_start"
68            | "on_session_end"
69            | "on_session_finalize"
70            | "subagent_stop" => {
71                return CallerKind::Hermes;
72            }
73            _ => {}
74        }
75
76        if has("tool_name") && has("tool_input") {
77            return CallerKind::ClaudeCode;
78        }
79        if has("type") && has("toolCall") {
80            return CallerKind::Cursor;
81        }
82        if has("event") && has("parameters") {
83            return CallerKind::Windsurf;
84        }
85        // Cline uses toolName (not toolCall)
86        if has("type") && has("toolName") && !has("toolCall") {
87            return CallerKind::Cline;
88        }
89        if has("kind") {
90            return CallerKind::Amp;
91        }
92    }
93
94    CallerKind::Unknown
95}
96
97// ---------------------------------------------------------------------------
98// Tests
99// ---------------------------------------------------------------------------
100
101#[cfg(test)]
102#[path = "detect_tests.rs"]
103mod tests;