1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Detects which agent harnesses are present on this system.
//!
//! galdr records the tool calls a harness emits, so knowing *which* harnesses are
//! installed — and whether galdr's sensor is wired into each — is part of its setup
//! and diagnostics story. Detection is read-only: it probes well-known config
//! directories and looks for the harness binary on `PATH`. It never runs them.
use std::path::PathBuf;
use directories::BaseDirs;
use serde::Serialize;
use crate::setup;
/// One detected (or absent) agent harness.
#[derive(Debug, Clone, Serialize)]
pub struct HarnessInfo {
/// Display name, e.g. "Claude Code".
pub name: String,
/// Stable key, e.g. "claude".
pub key: String,
/// True if a config directory or the binary was found.
pub detected: bool,
/// The harness config directory, if it exists.
pub config_dir: Option<String>,
/// Whether the harness binary is on `PATH`.
pub on_path: bool,
/// Whether galdr's hook is wired into this harness. `Some` only where galdr
/// knows how to wire one (today: Claude Code); `None` elsewhere.
pub galdr_hook: Option<bool>,
/// The directory this harness loads skills from, if galdr knows it and it
/// exists. A distilled skill must be reachable here to be usable.
pub skills_dir: Option<String>,
/// Short human note (e.g. hook status).
pub notes: String,
}
struct Known {
name: &'static str,
key: &'static str,
config: &'static str,
bin: &'static str,
/// Where this harness loads user skills from, relative to `$HOME`. `None`
/// when galdr does not yet know the harness's skills location.
skills_subdir: Option<&'static str>,
}
/// The harnesses galdr knows how to recognize. Ordered by how common they are.
/// `skills_subdir` is verified against on-disk layout, not assumed.
const KNOWN: &[Known] = &[
Known {
name: "Claude Code",
key: "claude",
config: ".claude",
bin: "claude",
skills_subdir: Some(".claude/skills"),
},
Known {
name: "Codex",
key: "codex",
config: ".codex",
bin: "codex",
skills_subdir: Some(".codex/skills"),
},
Known {
name: "Cursor",
key: "cursor",
config: ".cursor",
bin: "cursor",
skills_subdir: Some(".cursor/skills-cursor"),
},
Known {
name: "Gemini CLI",
key: "gemini",
config: ".gemini",
bin: "gemini",
skills_subdir: None,
},
Known {
name: "Aider",
key: "aider",
config: ".aider.conf.yml",
bin: "aider",
skills_subdir: None,
},
Known {
name: "Windsurf",
key: "windsurf",
config: ".windsurf",
bin: "windsurf",
skills_subdir: None,
},
];
/// Probes the system for known harnesses.
pub fn detect() -> Vec<HarnessInfo> {
let home = BaseDirs::new().map(|b| b.home_dir().to_path_buf());
KNOWN.iter().map(|k| info_for(k, home.as_ref())).collect()
}
/// Where a detected harness loads skills from, if galdr knows it. Used to make a
/// distilled skill discoverable across every installed harness.
pub fn skills_dir(key: &str) -> Option<PathBuf> {
let home = BaseDirs::new()?.home_dir().to_path_buf();
let known = KNOWN.iter().find(|k| k.key == key)?;
known.skills_subdir.map(|sub| home.join(sub))
}
fn info_for(k: &Known, home: Option<&PathBuf>) -> HarnessInfo {
let config_dir = home
.map(|h| h.join(k.config))
.filter(|p| p.exists())
.map(|p| p.display().to_string());
let on_path = binary_on_path(k.bin);
let galdr_hook = match k.key {
"claude" => setup::claude_hook_configured(),
"codex" => setup::codex_hook_configured(),
"cursor" => setup::cursor_hook_configured(),
_ => None,
};
let skills_dir = home
.zip(k.skills_subdir)
.map(|(h, sub)| h.join(sub))
.filter(|p| p.exists())
.map(|p| p.display().to_string());
let detected = config_dir.is_some() || on_path;
let notes = match galdr_hook {
Some(true) => "galdr sensor wired".to_string(),
Some(false) if detected => "galdr sensor not wired".to_string(),
_ => String::new(),
};
HarnessInfo {
name: k.name.to_string(),
key: k.key.to_string(),
detected,
config_dir,
on_path,
galdr_hook,
skills_dir,
notes,
}
}
/// True if an executable file named `bin` is found in any `PATH` directory.
fn binary_on_path(bin: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| dir.join(bin).is_file())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_returns_every_known_harness() {
let found = detect();
assert_eq!(found.len(), KNOWN.len());
assert!(found.iter().any(|h| h.key == "claude"));
// galdr can wire a hook into Claude Code, Codex, and Cursor (all have a native
// hooks file), so their flag may be Some depending on whether the file exists;
// the field is always well-formed and the entries are present.
let _ = found.iter().find(|h| h.key == "claude").unwrap().galdr_hook;
let _ = found.iter().find(|h| h.key == "codex").unwrap().galdr_hook;
let _ = found.iter().find(|h| h.key == "cursor").unwrap().galdr_hook;
}
#[test]
fn binary_on_path_finds_a_ubiquitous_binary() {
// `sh` exists on every unix PATH the tests run on.
assert!(binary_on_path("sh"));
assert!(!binary_on_path("definitely-not-a-real-binary-xyzzy"));
}
}