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.