use serde_json::{Value, json};
use super::ChangeKind;
use crate::diagnostics::{collect_wired_hooks, extract_hook_name, hook_key_for};
const MANAGED_HOOKS: &[(&str, u32)] = &[("user-prompt-submit", 10), ("session-start", 30)];
pub fn add_hooks(root: &mut Value, force: bool) -> Vec<ChangeKind> {
if root.as_object().is_none() {
*root = json!({});
}
{
let obj = root.as_object_mut().unwrap();
match obj.get("hooks") {
None => {
obj.insert("hooks".to_string(), json!({}));
}
Some(v) if !v.is_object() => {
obj.insert("hooks".to_string(), json!({}));
}
_ => {}
}
}
let wired = collect_wired_hooks(root);
let mut results = Vec::new();
for (name, timeout) in MANAGED_HOOKS {
let event_key = hook_key_for(name);
let command = format!("agentsec hook {name}");
let already_wired = wired.contains(*name);
if already_wired && !force {
results.push(ChangeKind::NoOp);
continue;
}
{
let hooks_obj = root.get_mut("hooks").unwrap().as_object_mut().unwrap();
match hooks_obj.get(event_key) {
None => {
hooks_obj.insert(event_key.to_string(), json!([]));
}
Some(v) if !v.is_array() => {
hooks_obj.insert(event_key.to_string(), json!([]));
}
_ => {}
}
}
let new_matcher = json!({
"matcher": "",
"hooks": [
{
"type": "command",
"command": command,
"timeout": timeout
}
]
});
root["hooks"][event_key]
.as_array_mut()
.unwrap()
.push(new_matcher);
results.push(if already_wired {
ChangeKind::Updated
} else {
ChangeKind::Added
});
}
results
}
pub fn remove_hooks(root: &mut Value) -> Vec<ChangeKind> {
let mut results = Vec::new();
for (name, _timeout) in MANAGED_HOOKS {
let event_key = hook_key_for(name);
let Some(matchers) = root
.get_mut("hooks")
.and_then(|h| h.get_mut(event_key))
.and_then(|v| v.as_array_mut())
else {
results.push(ChangeKind::NoOp);
continue;
};
let mut removed_any = false;
matchers.retain_mut(|matcher| {
if let Some(inner) = matcher.get_mut("hooks").and_then(|v| v.as_array_mut()) {
let before = inner.len();
inner.retain(|h| {
let is_agentsec = h
.get("command")
.and_then(|v| v.as_str())
.is_some_and(|cmd| extract_hook_name(cmd).is_some());
if is_agentsec {
removed_any = true;
}
!is_agentsec
});
let after = inner.len();
before == after || !inner.is_empty()
} else {
true
}
});
results.push(if removed_any {
ChangeKind::Removed
} else {
ChangeKind::NoOp
});
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn settings_with_hooks(event: &str, command: &str) -> Value {
json!({
"hooks": {
event: [
{
"matcher": "",
"hooks": [
{"type": "command", "command": command, "timeout": 10}
]
}
]
}
})
}
#[test]
fn add_hooks_adds_both_events() {
let mut root = json!({});
let changes = add_hooks(&mut root, false);
assert_eq!(changes.len(), 2);
assert_eq!(changes[0], ChangeKind::Added);
assert_eq!(changes[1], ChangeKind::Added);
let ups_cmd = root["hooks"]["UserPromptSubmit"][0]["hooks"][0]["command"]
.as_str()
.unwrap();
assert_eq!(ups_cmd, "agentsec hook user-prompt-submit");
let ss_cmd = root["hooks"]["SessionStart"][0]["hooks"][0]["command"]
.as_str()
.unwrap();
assert_eq!(ss_cmd, "agentsec hook session-start");
}
#[test]
fn add_hooks_dedup_skips_existing() {
let mut root = settings_with_hooks("UserPromptSubmit", "agentsec hook user-prompt-submit");
root["hooks"]["SessionStart"] = json!([{
"matcher": "",
"hooks": [{"type": "command", "command": "agentsec hook session-start", "timeout": 30}]
}]);
let changes = add_hooks(&mut root, false);
assert_eq!(changes[0], ChangeKind::NoOp);
assert_eq!(changes[1], ChangeKind::NoOp);
assert_eq!(
root["hooks"]["UserPromptSubmit"].as_array().unwrap().len(),
1
);
}
#[test]
fn add_hooks_defensive_init_no_hooks_key() {
let mut root = json!({"someOtherKey": "value"});
let changes = add_hooks(&mut root, false);
assert_eq!(changes[0], ChangeKind::Added);
assert!(root["hooks"].is_object());
}
#[test]
fn remove_hooks_removes_agentsec_entries() {
let mut root = json!({
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{"type": "command", "command": "agentsec hook user-prompt-submit", "timeout": 10},
{"type": "command", "command": "other-tool hook something", "timeout": 5}
]
}
],
"SessionStart": [
{
"matcher": "",
"hooks": [
{"type": "command", "command": "agentsec hook session-start", "timeout": 30}
]
}
]
}
});
let changes = remove_hooks(&mut root);
assert_eq!(changes[0], ChangeKind::Removed);
assert_eq!(changes[1], ChangeKind::Removed);
let ups_hooks = &root["hooks"]["UserPromptSubmit"][0]["hooks"];
assert_eq!(ups_hooks.as_array().unwrap().len(), 1);
assert_eq!(ups_hooks[0]["command"], "other-tool hook something");
assert_eq!(root["hooks"]["SessionStart"].as_array().unwrap().len(), 0);
}
#[test]
fn remove_hooks_noop_when_absent() {
let mut root = json!({"hooks": {"UserPromptSubmit": [], "SessionStart": []}});
let changes = remove_hooks(&mut root);
assert_eq!(changes[0], ChangeKind::NoOp);
assert_eq!(changes[1], ChangeKind::NoOp);
}
#[test]
fn remove_hooks_preserves_non_agentsec_entries() {
let mut root = json!({
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{"type": "command", "command": "some-other-tool run", "timeout": 5}
]
}
],
"SessionStart": []
}
});
let changes = remove_hooks(&mut root);
assert_eq!(changes[0], ChangeKind::NoOp);
assert_eq!(
root["hooks"]["UserPromptSubmit"][0]["hooks"]
.as_array()
.unwrap()
.len(),
1
);
}
}