Every tool call evaluated by agentjail produces exactly one of three verdicts: allow, ask, or deny.
Allow
A call is allowed when no deny or ask rule in the active policy fires.
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 an ask rule fires and no deny rule
fires. 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).
Deny
A call is denied when at least one deny rule fires and produces a message.
It only takes one rule to block a call: if multiple rules match, the call is
still denied (with the message from whichever rule fired, or all of them,
depending on how the policy is structured).
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 msg binding in the matching Rego
rule. For example, if this rule fires:
deny[msg] {
input.tool_name == "Bash"
contains(input.tool_input.command, "/.ssh/")
msg := "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
You can evaluate any tool call against your active policy directly from the command line 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.
The deny-by-rule model
The semantics are worth stating plainly: allow is the default. If no rule matches, the call goes through. 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.
See The policy model for how deny and ask
rules are written and what fields are available in input.