← All docs

Testing policies

Iterate on rules using opa test for unit tests and the agentjail-hook pipe for end-to-end verification.

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.

  1. Write the rule in ~/.agentjail/rules/my_policy.rego.
  2. Run agentjail policy list to confirm the daemon sees the policy.
  3. Run the deny case via agentjail-hook pipe (or a _test.rego unit test).
  4. Run one or two allow cases with inputs that should not match.
  5. 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_name is case-sensitive. Use the exact string your agent sends (e.g. "Bash", not "bash").
  • Wrong field name: input.tool_input.command only 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.