There are two complementary ways to test agentjail policies: Rego unit tests
with opa test, and end-to-end hook tests by piping a PreToolUse payload to
agentjail-hook. There is no dedicated agentjail policy test command.
For the full CLI reference, see CLI reference.
End-to-end hook testing (daemon required)
The most direct test is to pipe the exact JSON the Claude hook sends, and observe the verdict. The daemon must be running.
Deny case: confirm the rule fires.
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"cat ~/.ssh/id_rsa"}}' \
| agentjail-hook
Expected output (exit code 2):
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked: command targets sensitive path ~/.ssh/"}}
Allow case: confirm a harmless call passes through.
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"cat README.md"}}' \
| agentjail-hook
Expected: exit code 0, permissionDecision is "allow".
Ask/confirm case: some default rules return "ask" instead of outright
denying. Exit code is 0 for ask, the same as allow; inspect
permissionDecision in the output to distinguish them.
If the allow case is also denied, a different rule is firing. Check
agentjail policy list to see which policies are active and narrow down which
rule matches.
Rego unit tests with opa test
For offline, fast, repeatable tests, write *_test.rego files alongside your
policy files. The daemon skips *_test.rego at runtime; they are for opa test
only.
# my_policy_test.rego
package my_policy_test
import data.my_policy
test_ssh_command_is_denied {
deny["Blocked: command targets ~/.ssh/"] with input as {
"hook_event": "PreToolUse",
"tool_name": "Bash",
"tool_input": {"command": "cat ~/.ssh/id_rsa"}
}
}
test_safe_command_is_allowed {
count(deny) == 0 with input as {
"hook_event": "PreToolUse",
"tool_name": "Bash",
"tool_input": {"command": "cat README.md"}
}
}
Run all tests in the rules directory:
opa test ~/.agentjail/rules/
opa test exits nonzero if any test fails, making it safe to run in CI.
The basic workflow
Write a rule, then immediately cover the two cases that matter: one input that should be denied, and one that should pass through.
- Write the rule in
~/.agentjail/rules/my_policy.rego. - Run
agentjail policy listto confirm the daemon sees the policy. - Run the deny case via
agentjail-hookpipe (or a_test.regounit test). - Run one or two allow cases with inputs that should not match.
- If both behave as expected, the rule is ready.
Iterating on a rule
If a rule is not firing when you expect it to, check these common issues:
- Wrong tool name:
input.tool_nameis case-sensitive. Use the exact string your agent sends (e.g."Bash", not"bash"). - Wrong field name:
input.tool_input.commandonly exists for Bash calls. For other tools, the field names differ. See The input schema. - Condition not holding: Rego rules are conjunctions. If any line is false or undefined, the rule does not fire. Simplify the rule to a single condition and add conditions back one at a time.
Testing across multiple tools
Pass the tool_input shape that matches the tool you want to test:
# Bash
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls"}}' \
| agentjail-hook
# Write
echo '{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"/etc/passwd"}}' \
| agentjail-hook
# Read
echo '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"~/.aws/credentials"}}' \
| agentjail-hook
See The input schema for the correct
tool_input fields for each tool.
Checking which rule fired
If a call is denied and you have multiple policies loaded, the denial message
tells you which rule matched. Make your msg strings descriptive enough to
distinguish rules at a glance:
msg := "Blocked [my_policy/ssh-guard]: command targets ~/.ssh/"
That way, the permissionDecisionReason in the hook output tells you exactly
which rule and policy fired. For a broader view, agentjail logs -v adds a
summary line per event that includes the command or file path, the reason, and
the session ID.