← All docs

Verdicts

The three verdicts agentjail can return, what triggers each one, and what the agent receives for each outcome.

Every tool call evaluated by agentjail produces exactly one of three verdicts: allow, ask, or deny.

The verdict is chosen by resolver.rego, which collects all candidate entries from every loaded policy file and picks the most restrictive: deny > ask > allow. When no candidate fires at all, the default is ask (fail-safe: unknown calls escalate to the user rather than silently proceeding).

Allow

A call is allowed when the resolver’s effective_candidate set contains at least one allow entry and no ask or deny entry. Allowed calls pass through to the shell, filesystem, or network exactly as the agent intended. agentjail exits with status code 0 and does nothing further.

Ask

A call produces an ask verdict when the most restrictive candidate is ask (no deny candidate fired). The agent is prompted to confirm before the call proceeds. agentjail exits with status code 0 for ask verdicts (the agent handles the confirmation UI).

The fail-safe default is ask. If no candidate fires at all, for example, if no rule matches the incoming tool call, the resolver returns ask rather than silently allowing the call. This ensures that unexpected or novel tool calls escalate to the user.

Deny

A call is denied when at least one deny candidate fires in any loaded policy file. It only takes one rule to block a call. The winning deny candidate is the one with the lexicographically smallest rule_id (deterministic for audit replay).

When a call is denied:

  • agentjail exits with status code 2 (the Claude fast-block convention).
  • A structured block message is returned to the agent describing why the call was blocked.
  • The command never reaches the shell. No side effects occur.

What the agent sees

The hook returns a Claude-format JSON verdict to the agent:

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

The permissionDecision field is "allow", "ask", or "deny". The permissionDecisionReason comes from the reason field of the winning candidate object. For example, if this candidate fires:

r := {
  "action":  "deny",
  "rule_id": "command_policy/no-ssh-access",
  "reason":  "Blocked: command targets sensitive path ~/.ssh/",
}

The agent receives the JSON above with permissionDecision: "deny" and stops rather than proceeding. In practice, most agents will report the denial to the user and wait for further instruction.

Testing a verdict

Use agentjail try to evaluate any action against the live policy without executing anything:

agentjail try "cat ~/.ssh/id_rsa"   # ✗ DENY
agentjail try "git status"          # ✓ ALLOW
agentjail try                       # interactive: type commands, Ctrl-D to quit

You can also pipe a raw PreToolUse payload to the hook while the daemon is running:

echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf ~/.ssh/"}}' \
  | agentjail-hook

A denied call exits with code 2 and prints the denial reason. An allowed call exits with code 0.

You can also unit-test your Rego policy files directly with opa test without needing the daemon running.

Fail-open on internal error

If the hook encounters an internal error (for example, cannot reach the daemon), it fails open: it allows the call and exits with code 0. This ensures agent work is not silently interrupted by infrastructure issues, but you should monitor agentjail status and agentjail logs to catch daemon problems early.

Final outcome: policy meets the sandbox

The three verdicts above are the policy decision: what the rules say should happen before a call runs. When agentjail runs an agent sandboxed (the default; see Sandbox), there is a second line of defense: the OS sandbox (Landlock on Linux, Seatbelt on macOS) can block an action the policy allowed. agentjail reports the combined final outcome so you always see what truly happened and who enforced it.

For example, a command the policy allows but the sandbox blocks, such as cat ~/.ssh/id_rsa, is recorded as blocked · sandbox, never as a misleading green allow. A policy deny is reported as blocked by policy. The Monitor tab and agentjail logs show this final per-action result together with the responsible enforcer, so an action stopped by the sandbox is never displayed as allowed.

Mechanically, agentjail correlates the two hook phases by tool-use id: the PreToolUse hook records the policy verdict, and a PostToolUse hook inspects the tool result for the sandbox’s EPERM / “Operation not permitted” signature and records the real outcome. This is why the OS sandbox, not the bypassable command match, is the real boundary, with a policy deny acting as best-effort defense on top.

The candidate model

The semantics are worth stating plainly: every policy file contributes candidates; the resolver picks the most restrictive. You write rules to deny (or ask about) specific things rather than rules to allow a specific set of things. This keeps policies focused and easy to read: each rule expresses exactly one thing you want to block or confirm.

The fail-safe default of ask (not allow) means an uncovered tool call never silently proceeds. It surfaces to the user first.

See The policy model for how candidate rules are written and what fields are available in input.