# 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](/docs/reference/cli).

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

```sh
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"cat ~/.ssh/id_rsa"}}' \
  | agentjail-hook
```

Expected output (exit code `2`):

```json
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked: command targets sensitive path ~/.ssh/"}}
```

**Allow case:** confirm a harmless call passes through.

```sh
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.

```rego
# 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:

```sh
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](/docs/policies/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:

```sh
# 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](/docs/policies/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:

```rego
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.
