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