1use crate::types::CallerKind;
2
3pub fn detect_caller(stdin: &serde_json::Value) -> CallerKind {
11 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 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 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 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 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#[cfg(test)]
84#[path = "detect_tests.rs"]
85mod tests;