#[allow(clippy::wildcard_imports)]
use super::*;
pub(crate) fn check_gemini_trust_and_hooks(home: &std::path::Path, binary: &str) -> NamedCheck {
let settings = home.join(".gemini").join("settings.json");
if !settings.exists() {
return NamedCheck {
name: "Gemini hooks".to_string(),
ok: false,
detail: format!("missing ({})", settings.display()),
};
}
let content = std::fs::read_to_string(&settings).unwrap_or_default();
let parsed = crate::core::jsonc::parse_jsonc(&content).ok();
let Some(v) = parsed else {
return NamedCheck {
name: "Gemini hooks".to_string(),
ok: false,
detail: format!("invalid JSON ({})", settings.display()),
};
};
let trust_ok = v
.get("mcpServers")
.and_then(|m| m.get("lean-ctx"))
.and_then(|e| e.get("trust"))
.and_then(serde_json::Value::as_bool)
== Some(true);
let hooks_ok = v
.get("hooks")
.and_then(|h| h.get("BeforeTool"))
.and_then(|x| x.as_array())
.is_some_and(|arr| {
let mut saw_rewrite = false;
let mut saw_redirect = false;
for entry in arr {
let hooks = entry
.get("hooks")
.and_then(|x| x.as_array())
.cloned()
.unwrap_or_default();
for h in hooks {
let cmd = h
.get("command")
.and_then(|c| c.as_str())
.unwrap_or_default();
let first = cmd.split_whitespace().next().unwrap_or_default();
if cmd.contains("hook rewrite") && cmd_matches_expected(first, binary) {
saw_rewrite = true;
}
if cmd.contains("hook redirect") && cmd_matches_expected(first, binary) {
saw_redirect = true;
}
}
}
saw_rewrite && saw_redirect
});
let scripts_ok = home
.join(".gemini")
.join("hooks")
.join("lean-ctx-rewrite-gemini.sh")
.exists()
&& home
.join(".gemini")
.join("hooks")
.join("lean-ctx-redirect-gemini.sh")
.exists();
let ok = trust_ok && hooks_ok && scripts_ok;
NamedCheck {
name: "Gemini hooks".to_string(),
ok,
detail: if ok {
format!("ok ({})", settings.display())
} else {
"drift (hooks/trust/scripts)".to_string()
},
}
}
pub(crate) fn check_antigravity_cli_hooks(home: &std::path::Path, binary: &str) -> NamedCheck {
let name = "Antigravity CLI plugin".to_string();
let plugin_dir = crate::hooks::agents::antigravity_cli_plugin_dir(home);
let hooks_json = plugin_dir.join("hooks").join("hooks.json");
if !hooks_json.exists() {
return NamedCheck {
name,
ok: false,
detail: format!("missing ({})", hooks_json.display()),
};
}
let Some(v) = std::fs::read_to_string(&hooks_json)
.ok()
.and_then(|c| crate::core::jsonc::parse_jsonc(&c).ok())
else {
return NamedCheck {
name,
ok: false,
detail: format!("invalid JSON ({})", hooks_json.display()),
};
};
let observe_ok = v
.get("hooks")
.and_then(|h| h.get("PostToolUse"))
.and_then(|x| x.as_array())
.is_some_and(|arr| {
arr.iter().any(|entry| {
entry
.get("hooks")
.and_then(|x| x.as_array())
.is_some_and(|hooks| {
hooks.iter().any(|h| {
let cmd = h
.get("command")
.and_then(|c| c.as_str())
.unwrap_or_default();
let first = cmd.split_whitespace().next().unwrap_or_default();
cmd.contains("hook observe") && cmd_matches_expected(first, binary)
})
})
})
});
let manifest =
crate::hooks::agents::antigravity_cli_config_dir(home).join("import_manifest.json");
let registered = std::fs::read_to_string(&manifest)
.ok()
.and_then(|c| crate::core::jsonc::parse_jsonc(&c).ok())
.and_then(|v| {
v.get("imports").and_then(|i| i.as_array()).map(|a| {
a.iter()
.any(|e| e.get("name").and_then(|n| n.as_str()) == Some("lean-ctx"))
})
})
.unwrap_or(false);
let mcp_config = plugin_dir.join("mcp_config.json");
let mcp_ok = std::fs::read_to_string(&mcp_config)
.ok()
.and_then(|c| crate::core::jsonc::parse_jsonc(&c).ok())
.and_then(|v| {
v.get("mcpServers")
.and_then(|s| s.get("lean-ctx"))
.and_then(|s| s.get("command"))
.and_then(|c| c.as_str())
.map(|cmd| cmd_matches_expected(cmd, binary))
})
.unwrap_or(false);
let ok = observe_ok && registered && mcp_ok;
NamedCheck {
name,
ok,
detail: if ok {
format!("ok ({})", plugin_dir.display())
} else if !registered {
format!(
"not registered in import_manifest.json ({})",
plugin_dir.display()
)
} else if !mcp_ok {
format!(
"missing/stale plugin mcp_config.json ({})",
mcp_config.display()
)
} else {
format!("drift (observe hook) ({})", hooks_json.display())
},
}
}
pub(crate) fn antigravity_cli_hooks_note() -> NamedCheck {
NamedCheck {
name: "Antigravity CLI hook gating".to_string(),
ok: true,
detail: "hook execution is gated server-side by agy's enable_json_hooks experiment (no local config can force it) — if /hooks shows lean-ctx dormant, the plugin is still installed correctly; verify with `agy plugin validate ~/.gemini/config/plugins/lean-ctx`. The ctx_* MCP tools compress on every surface regardless.".to_string(),
}
}
pub(crate) fn check_cursor_hooks(path: &std::path::Path, binary: &str) -> NamedCheck {
if !path.exists() {
return NamedCheck {
name: "Hooks".to_string(),
ok: false,
detail: format!("missing ({})", path.display()),
};
}
let content = std::fs::read_to_string(path).unwrap_or_default();
let parsed = crate::core::jsonc::parse_jsonc(&content).ok();
let Some(v) = parsed else {
return NamedCheck {
name: "Hooks".to_string(),
ok: false,
detail: format!("invalid JSON ({})", path.display()),
};
};
let pre = v
.get("hooks")
.and_then(|h| h.get("preToolUse"))
.and_then(|x| x.as_array())
.cloned()
.unwrap_or_default();
let has_rewrite = pre.iter().any(|e| {
e.get("matcher").and_then(|m| m.as_str()) == Some("Shell")
&& e.get("command")
.and_then(|c| c.as_str())
.is_some_and(|c| c.contains(" hook rewrite"))
});
let has_redirect = pre.iter().any(|e| {
matches!(
e.get("matcher").and_then(|m| m.as_str()),
Some("Read|Grep|Glob" | "Read|Grep" | "Read" | "Grep")
) && e
.get("command")
.and_then(|c| c.as_str())
.is_some_and(|c| c.contains(" hook redirect"))
});
let entries_ok = has_rewrite && has_redirect;
let stale = stale_hook_binary(&content, binary);
finalize_hook_check("Hooks", path, entries_ok, stale)
}
pub(crate) fn finalize_hook_check(
name: &str,
path: &std::path::Path,
entries_ok: bool,
stale: Option<String>,
) -> NamedCheck {
let ok = entries_ok && stale.is_none();
let detail = if !entries_ok {
format!("drift ({})", path.display())
} else if let Some(old) = stale {
format!("stale binary {old} — run lean-ctx setup --fix")
} else {
format!("ok ({})", path.display())
};
NamedCheck {
name: name.to_string(),
ok,
detail,
}
}
pub(crate) fn check_hook_wrapper_scripts(
hooks_dir: &std::path::Path,
binary: &str,
home: &std::path::Path,
) -> NamedCheck {
let wrapper_names = [
"lean-ctx-rewrite.sh",
"lean-ctx-rewrite-native",
"lean-ctx-redirect-native",
];
let mut stale: Option<String> = None;
let mut seen = 0usize;
for name in wrapper_names {
let path = hooks_dir.join(name);
let Ok(content) = std::fs::read_to_string(&path) else {
continue;
};
seen += 1;
let Some(token) = crate::hooks::wrapper_binary_token(&content) else {
continue;
};
let ok = super::wiring::cmd_matches_expected(&token, binary)
|| crate::hooks::wrapper_content_is_portable_and_working(&content, home);
if !ok && stale.is_none() {
stale = Some(format!("{token} ({name})"));
}
}
let (ok, detail) = if seen == 0 {
(true, format!("not installed ({})", hooks_dir.display()))
} else if let Some(s) = stale {
(
false,
format!("stale binary {s} — run lean-ctx setup --fix"),
)
} else {
(true, format!("ok ({seen} wrapper scripts)"))
};
NamedCheck {
name: "Hook wrappers".to_string(),
ok,
detail,
}
}
pub(crate) fn check_claude_hooks(path: &std::path::Path, binary: &str) -> NamedCheck {
if !path.exists() {
return NamedCheck {
name: "Hooks".to_string(),
ok: false,
detail: format!("missing ({})", path.display()),
};
}
let content = std::fs::read_to_string(path).unwrap_or_default();
let parsed = crate::core::jsonc::parse_jsonc(&content).ok();
let Some(v) = parsed else {
return NamedCheck {
name: "Hooks".to_string(),
ok: false,
detail: format!("invalid JSON ({})", path.display()),
};
};
let pre = v
.get("hooks")
.and_then(|h| h.get("PreToolUse"))
.and_then(|x| x.as_array())
.cloned()
.unwrap_or_default();
let joined = serde_json::to_string(&pre).unwrap_or_default();
let entries_ok = joined.contains(" hook rewrite") && joined.contains(" hook redirect");
let stale = stale_hook_binary(&joined, binary);
finalize_hook_check("Hooks", path, entries_ok, stale)
}