use crate::hooks::Hook;
use crate::hooks::HookAction;
use crate::hooks::context::PreToolUseContext;
pub struct BlocklistHook {
blocked: Vec<String>,
allowed: Vec<String>,
}
impl BlocklistHook {
#[must_use]
pub fn deny(blocked: Vec<String>) -> Self {
Self {
blocked,
allowed: Vec::new(),
}
}
#[must_use]
pub fn allow_only(allowed: Vec<String>) -> Self {
Self {
blocked: Vec::new(),
allowed,
}
}
}
impl Hook for BlocklistHook {
fn name(&self) -> &'static str {
"blocklist"
}
fn on_pre_tool_use(&self, ctx: &PreToolUseContext) -> Option<HookAction> {
if self.blocked.iter().any(|b| b == &ctx.tool_name) {
return Some(HookAction::block(format!(
"Tool '{}' is blocked by policy",
ctx.tool_name
)));
}
if !self.allowed.is_empty() && !self.allowed.iter().any(|a| a == &ctx.tool_name) {
return Some(HookAction::block(format!(
"Tool '{}' is not in the allowlist",
ctx.tool_name
)));
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_ctx(tool_name: &str) -> PreToolUseContext {
PreToolUseContext {
tool_name: tool_name.to_string(),
input: json!({}),
session_id: uuid::Uuid::nil(),
turn_number: 0,
}
}
#[test]
fn deny_blocks_listed_tools() {
let hook = BlocklistHook::deny(vec!["rm_rf".to_string()]);
let action = hook.on_pre_tool_use(&make_ctx("rm_rf")).unwrap();
assert!(action.is_block());
}
#[test]
fn deny_allows_unlisted_tools() {
let hook = BlocklistHook::deny(vec!["rm_rf".to_string()]);
assert!(hook.on_pre_tool_use(&make_ctx("read_file")).is_none());
}
#[test]
fn allow_only_permits_listed_tools() {
let hook = BlocklistHook::allow_only(vec!["read_file".to_string()]);
assert!(hook.on_pre_tool_use(&make_ctx("read_file")).is_none());
}
#[test]
fn allow_only_blocks_unlisted_tools() {
let hook = BlocklistHook::allow_only(vec!["read_file".to_string()]);
let action = hook.on_pre_tool_use(&make_ctx("rm_rf")).unwrap();
assert!(action.is_block());
}
#[test]
fn deny_takes_precedence_over_allow() {
let hook = BlocklistHook {
blocked: vec!["rm_rf".to_string()],
allowed: vec!["rm_rf".to_string()],
};
let action = hook.on_pre_tool_use(&make_ctx("rm_rf")).unwrap();
assert!(action.is_block());
}
#[test]
fn empty_allowlist_allows_everything() {
let hook = BlocklistHook::allow_only(Vec::new());
assert!(hook.on_pre_tool_use(&make_ctx("anything")).is_none());
}
#[test]
fn block_reason_contains_tool_name() {
let hook = BlocklistHook::deny(vec!["dangerous".to_string()]);
match hook.on_pre_tool_use(&make_ctx("dangerous")) {
Some(HookAction::Block { reason }) => {
assert!(reason.contains("dangerous"));
}
other => panic!("expected Block, got {other:?}"),
}
}
}