use serde_json::json;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DenyShape {
ClaudeNested,
CopilotFlat,
CursorPermission,
Decision(&'static str),
}
pub fn render_deny(shape: DenyShape, reason: &str) -> String {
let value = match shape {
DenyShape::ClaudeNested => json!({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason,
}
}),
DenyShape::CopilotFlat => json!({
"permissionDecision": "deny",
"permissionDecisionReason": reason,
}),
DenyShape::CursorPermission => json!({
"permission": "deny",
"agentMessage": reason,
"agent_message": reason,
}),
DenyShape::Decision(verdict) => json!({
"decision": verdict,
"reason": reason,
}),
};
value.to_string()
}
pub fn should_deny(event: &str, deny_substr: &str) -> bool {
!deny_substr.is_empty() && event.contains(deny_substr)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
fn parsed(shape: DenyShape) -> Value {
serde_json::from_str(&render_deny(shape, "oneharness: blocked")).unwrap()
}
#[test]
fn claude_nested_shape_matches_the_protocol() {
let v = parsed(DenyShape::ClaudeNested);
assert_eq!(v["hookSpecificOutput"]["hookEventName"], "PreToolUse");
assert_eq!(v["hookSpecificOutput"]["permissionDecision"], "deny");
assert_eq!(
v["hookSpecificOutput"]["permissionDecisionReason"],
"oneharness: blocked"
);
}
#[test]
fn copilot_is_flat() {
let v = parsed(DenyShape::CopilotFlat);
assert_eq!(v["permissionDecision"], "deny");
assert!(v.get("hookSpecificOutput").is_none());
}
#[test]
fn cursor_carries_both_message_spellings() {
let v = parsed(DenyShape::CursorPermission);
assert_eq!(v["permission"], "deny");
assert_eq!(v["agentMessage"], "oneharness: blocked");
assert_eq!(v["agent_message"], "oneharness: blocked");
}
#[test]
fn decision_field_carries_deny_or_block() {
assert_eq!(parsed(DenyShape::Decision("deny"))["decision"], "deny");
assert_eq!(parsed(DenyShape::Decision("block"))["decision"], "block");
assert_eq!(
parsed(DenyShape::Decision("block"))["reason"],
"oneharness: blocked"
);
}
#[test]
fn should_deny_substring_matches_command_in_event() {
let event = r#"{"tool_name":"Bash","tool_input":{"command":"touch DENYME-123.txt"}}"#;
assert!(should_deny(event, "DENYME-123"));
assert!(!should_deny(event, "OTHER-MARKER"));
assert!(!should_deny(event, ""), "an empty needle must never match");
}
}