The mod's logic, compiled to wasm32 and then to JavaScript by wasm2js
(a mod's realm has no WebAssembly). The plugin's bridge.ts is the only
caller: it writes a JSON question into linear memory, calls handle, and
reads the JSON answer back. handle_json is the same logic as plain Rust,
so it is tested natively.
The question is what to do with one hook event, answered inside Claude Code's own process in well under a millisecond: send it to the daemon and wait, send it without waiting, or leave it alone.
11mod read_only;
One event, as much of it as routing needs.
The tool, for the tool events.
22 tool: Option<String>,
What to do with routed, and why when it is not the event's usual role.
28fn route(routed: &Routed) -> (Role, &'static str) { 29 let role = routed.event.role(); 30 let about_a_tool = matches!( 31 routed.event, 32 HookEvent::PreToolUse | HookEvent::PostToolUse | HookEvent::PostToolUseFailure | HookEvent::PermissionDenied 33 ); 34 if !about_a_tool { 35 return (role, ""); 36 } 37 // Only Bash is judged, so only Bash's outcomes are worth reporting. 38 if routed.tool.as_deref() != Some("Bash") { 39 return (Role::Ignore, "only Bash commands are judged"); 40 } 41 match routed.command.as_deref() { 42 Some(command) if read_only::is_read_only(command) => (Role::Ignore, "plainly read-only"), 43 _ => (role, ""), 44 } 45}
Answers one routing question's JSON with JSON: { "role": … }, with a
why when the event's usual role was overridden. An event this build does
not know is ignored, and the answer says so.
50pub fn handle_json(input: &[u8]) -> Vec<u8> { 51 let answer = match serde_json::from_slice::<Routed>(input) { 52 Ok(routed) => { 53 let (role, why) = route(&routed); 54 json!({ "role": role, "why": why }) 55 } 56 Err(error) => json!({ "role": Role::Ignore, "why": format!("not routed: {error}") }), 57 }; 58 serde_json::to_vec(&answer).unwrap_or_default() 59}
Reserves len bytes for the caller to fill.
Releases bytes from alloc or handle.
Safety
ptr and len must come from one alloc(len), or from handle and out_len.
Answers the JSON at ptr..ptr+len; the answer's length is out_len().
Safety
ptr..ptr+len must be initialised bytes the caller owns.
87 #[unsafe(no_mangle)] 88 pub unsafe extern "C" fn handle(ptr: *const u8, len: usize) -> *mut u8 { 89 let input = unsafe { std::slice::from_raw_parts(ptr, len) }; 90 let mut bytes = super::handle_json(input).into_boxed_slice(); 91 unsafe { OUT_LEN = bytes.len() }; 92 let out = bytes.as_mut_ptr(); 93 std::mem::forget(bytes); 94 out 95 }
Length of the last answer from handle.
104#[cfg(test)] 105mod tests { 106 use super::handle_json; 107 use serde_json::{Value, json}; 108 109 fn role(input: Value) -> Value { 110 let answer: Value = serde_json::from_slice(&handle_json(input.to_string().as_bytes())).unwrap(); 111 answer["role"].clone() 112 } 113 114 #[test] 115 fn a_risky_bash_command_is_decided() { 116 assert_eq!(role(json!({ "event": "PreToolUse", "tool": "Bash", "command": "rm -rf target" })), "decide"); 117 } 118 119 #[test] 120 fn a_read_only_bash_command_is_never_sent() { 121 assert_eq!(role(json!({ "event": "PreToolUse", "tool": "Bash", "command": "git status" })), "ignore"); 122 assert_eq!(role(json!({ "event": "PostToolUse", "tool": "Bash", "command": "git status" })), "ignore"); 123 } 124 125 #[test] 126 fn other_tools_are_not_judged() { 127 assert_eq!(role(json!({ "event": "PreToolUse", "tool": "Read" })), "ignore"); 128 assert_eq!(role(json!({ "event": "PostToolUse", "tool": "Edit" })), "ignore"); 129 } 130 131 #[test] 132 fn a_judged_commands_outcome_is_observed() { 133 assert_eq!(role(json!({ "event": "PostToolUse", "tool": "Bash", "command": "rm -rf target" })), "observe"); 134 assert_eq!(role(json!({ "event": "PermissionDenied", "tool": "Bash", "command": "rm -rf target" })), "observe"); 135 } 136 137 #[test] 138 fn events_keep_their_roles() { 139 assert_eq!(role(json!({ "event": "Stop" })), "decide"); 140 assert_eq!(role(json!({ "event": "UserPromptSubmit" })), "observe"); 141 assert_eq!(role(json!({ "event": "MessageDisplay" })), "ignore"); 142 } 143 144 #[test] 145 fn an_unknown_event_is_ignored() { 146 assert_eq!(role(json!({ "event": "SomethingNew" })), "ignore"); 147 assert_eq!(role(json!("text")), "ignore"); 148 } 149}