use serde_json::Value;
pub fn prompt_of(data: &Value) -> Option<&str> {
field(data, &["prompt"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn tool_name_of(data: &Value) -> Option<&str> {
field(data, &["tool_name", "toolName"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub const TOOL_INPUT_KEYS: &[&str] = &["tool_input", "parameters"];
pub const TOOL_RESULT_KEYS: &[&str] = &["tool_result", "result", "tool_response"];
pub fn tool_input_of(data: &Value) -> Option<&Value> {
field(data, TOOL_INPUT_KEYS)
}
pub fn tool_ok_of(data: &Value) -> Option<bool> {
if let Some(success) = field(data, &["success"]).and_then(Value::as_bool) {
return Some(success);
}
tool_result_of(data)?
.get("is_error")
.and_then(Value::as_bool)
.map(|is_error| !is_error)
}
pub fn duration_ms_of(data: &Value) -> Option<u64> {
field(data, &["duration_ms", "durationMs", "executionTimeMs"])?.as_u64()
}
pub fn native_event_of(data: &Value) -> Option<&str> {
field(data, &["hook_event_name", "hookName"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn turn_id_of(data: &Value) -> Option<&str> {
field(data, &["turn_id", "turnId"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn agent_version_of(data: &Value) -> Option<&str> {
field(data, &["agent_version", "clineVersion"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn tool_result_of(data: &Value) -> Option<&Value> {
field(data, TOOL_RESULT_KEYS)
}
pub fn tool_use_id_of(data: &Value) -> Option<&str> {
field(data, &["tool_use_id", "toolUseId"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn cwd_of(data: &Value) -> Option<&str> {
if let Some(cwd) = field(data, &["cwd"])
.and_then(Value::as_str)
.and_then(non_empty)
{
return Some(cwd);
}
field(data, &["workspaceRoots"])?
.as_array()?
.first()?
.as_str()
.and_then(non_empty)
}
pub fn session_id_of(data: &Value) -> Option<&str> {
field(data, &["session_id", "sessionId", "taskId"])
.and_then(Value::as_str)
.and_then(non_empty)
}
pub fn prompt_and_mode(data: &Value) -> (Option<&str>, Option<&str>) {
let Some(raw) = prompt_of(data) else {
return (None, None);
};
let Some(rest) = raw.strip_prefix("<user_input") else {
return (Some(raw), None);
};
let Some((attrs, body)) = rest.split_once('>') else {
return (Some(raw), None);
};
let Some(inner) = body.strip_suffix("</user_input>") else {
return (Some(raw), None);
};
let mode = attrs
.split_once("mode=\"")
.and_then(|(_, m)| m.split_once('"'))
.map(|(m, _)| m)
.and_then(non_empty);
(non_empty(inner.trim()).or(Some(raw)), mode)
}
pub fn to_common(data: &Value) -> Option<Value> {
let mut common = serde_json::Map::new();
let (prompt, mode) = prompt_and_mode(data);
let mut put = |key: &str, value: Option<Value>| {
if let Some(value) = value {
common.insert(key.to_string(), value);
}
};
let text = |v: Option<&str>| v.map(|s| Value::String(s.to_string()));
put("hook_event_name", text(native_event_of(data)));
put("session_id", text(session_id_of(data)));
put("cwd", text(cwd_of(data)));
put("prompt", text(prompt));
put("tool_name", text(tool_name_of(data)));
put("tool_input", tool_input_of(data).cloned());
put("tool_response", tool_result_of(data).cloned());
put("tool_use_id", text(tool_use_id_of(data)));
put("turn_id", text(turn_id_of(data)));
put("agent_version", text(agent_version_of(data)));
put("duration_ms", duration_ms_of(data).map(Value::from));
put("model", data.get("model").cloned());
if mode.is_none() && common.iter().all(|(k, v)| data.get(k) == Some(v)) {
return None;
}
let mut raw = data.clone();
if let (Some(mode), Some(obj)) = (mode, raw.as_object_mut()) {
obj.insert("mode".to_string(), Value::String(mode.to_string()));
}
common.insert("raw_agent_payload".to_string(), raw);
Some(Value::Object(common))
}
fn field<'a>(data: &'a Value, keys: &[&str]) -> Option<&'a Value> {
located(data, keys).map(|(_, value)| value)
}
pub fn pointer_to(data: &Value, keys: &[&str]) -> Option<String> {
located(data, keys).map(|(pointer, _)| pointer)
}
fn located<'a>(data: &'a Value, keys: &[&str]) -> Option<(String, &'a Value)> {
for key in keys {
if let Some(found) = data.get(key) {
return Some((format!("/{}", escape(key)), found));
}
}
let object = data.as_object()?;
keys.iter().find_map(|key| {
object
.iter()
.filter(|(outer, _)| !is_argument_container(outer))
.find_map(|(outer, nested)| {
nested
.get(key)
.map(|found| (format!("/{}/{}", escape(outer), escape(key)), found))
})
})
}
fn is_argument_container(key: &str) -> bool {
TOOL_INPUT_KEYS.contains(&key) || TOOL_RESULT_KEYS.contains(&key)
}
fn escape(token: &str) -> String {
token.replace('~', "~0").replace('/', "~1")
}
pub fn model_of(data: &Value) -> (Option<&str>, Option<&str>) {
let Some(model) = data.get("model") else {
return (None, None);
};
if let Some(slug) = model.as_str() {
return (None, non_empty(slug));
}
(
model
.get("provider")
.and_then(Value::as_str)
.and_then(non_empty),
model
.get("slug")
.and_then(Value::as_str)
.and_then(non_empty),
)
}
fn non_empty(s: &str) -> Option<&str> {
let t = s.trim();
(!t.is_empty()).then_some(s)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_prompt_is_found_by_shape_across_agents() {
assert_eq!(
prompt_of(&json!({"prompt": "hello", "session_id": "abc"})),
Some("hello")
);
assert_eq!(
prompt_of(&json!({
"hookName": "UserPromptSubmit",
"userPromptSubmit": {"prompt": "hey there !!"}
})),
Some("hey there !!")
);
assert_eq!(
prompt_of(&json!({"someFutureHook": {"prompt": "p"}})),
Some("p")
);
assert_eq!(prompt_of(&json!({"toolName": "Bash"})), None);
assert_eq!(prompt_of(&json!({"prompt": " "})), None);
}
#[test]
fn clines_nested_hook_shape_reads_the_same_as_a_flat_one() {
let nested = json!({
"taskId": "conv_1789665246332_em78z12",
"hookName": "PreToolUse",
"clineVersion": "4.1.17",
"workspaceRoots": ["/work/scratch"],
"preToolUse": {
"toolName": "execute_command",
"parameters": {"command": "rm -rf /tmp"}
}
});
assert_eq!(tool_name_of(&nested), Some("execute_command"));
assert_eq!(
tool_input_of(&nested),
Some(&json!({"command": "rm -rf /tmp"}))
);
assert_eq!(cwd_of(&nested), Some("/work/scratch"));
let post = json!({
"taskId": "conv_1",
"postToolUse": {"toolName": "execute_command", "result": "ok", "success": true}
});
assert_eq!(tool_result_of(&post), Some(&json!("ok")));
let flat = json!({"toolName": "execute_command", "parameters": {"command": "ls"}});
assert_eq!(tool_name_of(&flat), Some("execute_command"));
assert_eq!(tool_input_of(&flat), Some(&json!({"command": "ls"})));
}
#[test]
fn the_native_spelling_outranks_every_nested_one() {
let native = json!({
"session_id": "sess_a",
"cwd": "/repo",
"tool_name": "Bash",
"tool_input": {"command": "cargo test"},
"tool_use_id": "toolu_01",
"nested": {"tool_name": "NotThis", "tool_input": {"command": "nor this"}}
});
assert_eq!(tool_name_of(&native), Some("Bash"));
assert_eq!(
tool_input_of(&native),
Some(&json!({"command": "cargo test"}))
);
assert_eq!(tool_use_id_of(&native), Some("toolu_01"));
assert_eq!(cwd_of(&native), Some("/repo"));
let empty = json!({"hookName": "Stop", "taskId": "conv_1"});
assert_eq!(tool_name_of(&empty), None);
assert_eq!(tool_input_of(&empty), None);
assert_eq!(tool_result_of(&empty), None);
assert_eq!(tool_use_id_of(&empty), None);
assert_eq!(cwd_of(&empty), None);
}
#[test]
fn a_pointer_resolves_to_the_value_the_reader_returns() {
for data in [
json!({"tool_input": {"command": "cargo test"}}),
json!({"preToolUse": {"parameters": {"command": "cargo test"}}}),
json!({"parameters": {"command": "cargo test"}}),
] {
let pointer = pointer_to(&data, TOOL_INPUT_KEYS).expect("a pointer");
assert_eq!(
data.pointer(&pointer),
tool_input_of(&data),
"pointer {pointer} and reader disagree for {data}"
);
assert_eq!(
data.pointer(&pointer).and_then(|v| v.get("command")),
Some(&json!("cargo test"))
);
}
assert_eq!(pointer_to(&json!({"prompt": "hi"}), TOOL_INPUT_KEYS), None);
}
#[test]
fn pointer_tokens_are_escaped() {
let data = json!({"a/b": {"tool_input": 1}, "c~d": {"tool_input": 2}});
let pointer = pointer_to(&data, TOOL_INPUT_KEYS).expect("a pointer");
assert!(
pointer == "/a~1b/tool_input" || pointer == "/c~0d/tool_input",
"unexpected pointer: {pointer}"
);
assert!(data.pointer(&pointer).is_some(), "{pointer} must resolve");
}
#[test]
fn the_remaining_facts_read_across_agents() {
let cline_post = json!({
"taskId": "conv_1",
"hookName": "PostToolUse",
"clineVersion": "4.1.17",
"postToolUse": {
"toolName": "execute_command",
"result": "done",
"success": false,
"executionTimeMs": 1234
}
});
assert_eq!(tool_ok_of(&cline_post), Some(false));
assert_eq!(duration_ms_of(&cline_post), Some(1234));
assert_eq!(native_event_of(&cline_post), Some("PostToolUse"));
assert_eq!(agent_version_of(&cline_post), Some("4.1.17"));
assert_eq!(turn_id_of(&cline_post), None);
let claude_post = json!({
"hook_event_name": "PostToolUse",
"tool_name": "Bash",
"tool_response": {"is_error": true, "stdout": ""}
});
assert_eq!(tool_ok_of(&claude_post), Some(false));
assert_eq!(
tool_ok_of(&json!({"tool_response": {"is_error": false}})),
Some(true)
);
let codex = json!({"session_id": "s", "turn_id": "t_1", "hook_event_name": "PreToolUse"});
assert_eq!(turn_id_of(&codex), Some("t_1"));
assert_eq!(tool_ok_of(&json!({"tool_name": "Bash"})), None);
assert_eq!(duration_ms_of(&json!({"tool_name": "Bash"})), None);
assert_eq!(agent_version_of(&json!({"tool_name": "Bash"})), None);
}
#[test]
fn the_probe_never_descends_past_one_level() {
let deep = json!({"a": {"b": {"prompt": "buried", "tool_name": "Buried"}}});
assert_eq!(prompt_of(&deep), None);
assert_eq!(tool_name_of(&deep), None);
}
#[test]
fn the_model_is_read_without_inventing_the_missing_half() {
assert_eq!(
model_of(&json!({"model": {"provider": "ollama", "slug": "qwen2.5-coder:7b"}})),
(Some("ollama"), Some("qwen2.5-coder:7b"))
);
assert_eq!(
model_of(&json!({"model": "claude-opus-5"})),
(None, Some("claude-opus-5"))
);
assert_eq!(
model_of(&json!({"model": {"provider": "unknown", "slug": "unknown"}})),
(Some("unknown"), Some("unknown"))
);
assert_eq!(model_of(&json!({})), (None, None));
}
#[test]
fn clines_wrapper_is_removed_and_its_mode_kept() {
let data = json!({
"taskId": "conv_1",
"hookName": "UserPromptSubmit",
"userPromptSubmit": {"prompt": "<user_input mode=\"act\">hey y</user_input>"}
});
assert_eq!(prompt_and_mode(&data), (Some("hey y"), Some("act")));
let mapped = to_common(&data).expect("a wrapped prompt is a rewrite");
assert_eq!(mapped["prompt"], "hey y");
assert_eq!(mapped["raw_agent_payload"]["mode"], "act");
assert_eq!(
mapped["raw_agent_payload"]["userPromptSubmit"]["prompt"],
"<user_input mode=\"act\">hey y</user_input>"
);
}
#[test]
fn a_prompt_that_is_not_wrapped_is_untouched() {
for raw in [
"just a prompt",
"<user_input mode=\"act\">unterminated",
"look at <user_input mode=\"act\">this</user_input> inline",
] {
let data = json!({"prompt": raw});
let (prompt, _) = prompt_and_mode(&data);
assert_eq!(prompt, Some(raw), "must not rewrite {raw:?}");
}
let bare = json!({"prompt": "<user_input>hi</user_input>"});
assert_eq!(prompt_and_mode(&bare), (Some("hi"), None));
}
#[test]
fn a_tool_argument_is_never_mistaken_for_the_prompt() {
let task = json!({
"hook_event_name": "PreToolUse",
"tool_name": "Task",
"tool_input": {"description": "review", "prompt": "SUBAGENT PROMPT"}
});
assert_eq!(prompt_of(&task), None);
let shadowed = json!({
"tool_name": "Task",
"tool_input": {"cwd": "/tmp/sub", "toolName": "Inner", "prompt": "x"}
});
assert_eq!(cwd_of(&shadowed), None);
assert_eq!(tool_name_of(&shadowed), Some("Task"));
let cline = json!({"userPromptSubmit": {"prompt": "the real prompt"}});
assert_eq!(prompt_of(&cline), Some("the real prompt"));
let flat = json!({"tool_input": {"command": "ls"}, "tool_result": "ok"});
assert_eq!(tool_input_of(&flat), Some(&json!({"command": "ls"})));
assert_eq!(tool_result_of(&flat), Some(&json!("ok")));
}
}