← 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.

Quick verdict check with agentjail try

The easiest way to test a verdict is agentjail try. No JSON, no pipe:

agentjail try "cat ~/.ssh/id_rsa"   # expect: DENY
agentjail try "cat README.md"       # expect: ALLOW
agentjail try                       # interactive: type commands, Ctrl-D to quit

End-to-end hook testing (daemon required)

For low-level testing, pipe the exact JSON the agent 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 agentjail_test

import data.agentjail

test_ssh_command_is_denied {
  some c in agentjail.candidate
  c.action == "deny"
  c.rule_id == "custom/my_policy/no-ssh-access"
  with input as {
    "hook_event_name": "PreToolUse",
    "tool_name": "Bash",
    "tool_input": {"command": "cat ~/.ssh/id_rsa"}
  }
}

test_safe_command_produces_no_deny {
  not some c in agentjail.candidate { c.action == "deny" }
  with input as {
    "hook_event_name": "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 ~/my_policy.rego using the candidate contains r if { ... } pattern.
  2. Install it with agentjail policy add ~/my_policy.rego.
  3. Run agentjail policy list to confirm the daemon sees the rule.
  4. Run a quick check: agentjail try "cat ~/.ssh/id_rsa" (or pipe via agentjail-hook).
  5. Run one or two allow cases with inputs that should not match.
  6. 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:

r := {
  "action":  "deny",
  "rule_id": "custom/my_policy/no-ssh-access",
  "reason":  "Blocked [custom/my_policy/no-ssh-access]: command targets ~/.ssh/",
}

The permissionDecisionReason in the hook output will contain the reason string from the winning candidate, telling you exactly which rule 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. agentjail logs --action=deny --since=1h filters to just blocks from the last hour.