A policy file in package agentjail contributes candidate entries to the
shared resolver. Each candidate is an object with an action, a rule_id, and
a reason. resolver.rego collects all candidates from all loaded files and
picks the most restrictive verdict (deny > ask > allow). The fail-safe default
when nothing fires is ask.
This page walks through writing a single rule from scratch, installing it, and confirming it works.
Anatomy of a candidate rule
package agentjail
import future.keywords.if
import future.keywords.contains
candidate contains r if {
input.tool_name == "Bash"
contains(input.tool_input.command, "/.ssh/")
r := {
"action": "deny",
"rule_id": "custom/my_policy/no-ssh-access",
"reason": "Blocked: command targets sensitive path ~/.ssh/",
"impact": "would expose SSH private keys",
}
}
Breaking that down:
package agentjail: all hook-path rules must use this package.candidate contains r if { ... }: the partial rule entry pattern. Never declaredecision = ...in your own rule files:resolver.regois the only file that ownsdecision.ris an object with mandatoryaction,rule_id, andreason(plus an optionalimpact).actionmust be"deny","ask", or"allow".rule_idmust use thecustom/<filename_stem>/<rule>namespace (enforced byagentjail policy add).- The body is a conjunction: every line must hold for the candidate to be added.
If any condition does not hold, the candidate is not contributed. agentjail only
blocks the call if the resolver picks a deny candidate as the most restrictive.
See The input schema for the full shape of
input and how fields vary by tool.
Write a rule
Create a file called ~/my_policy.rego:
package agentjail
import future.keywords.if
import future.keywords.contains
candidate contains r if {
input.tool_name == "Bash"
contains(input.tool_input.command, ".env")
r := {
"action": "deny",
"rule_id": "custom/my_policy/no-env-access",
"reason": "Blocked: command reads or modifies .env file",
}
}
The rule_id must start with custom/my_policy/ (matching the filename stem
my_policy). The rule fires whenever a Bash command string contains .env.
Install and list policies
Install the rule using the CLI: this validates the authoring contract and hot-reloads the daemon:
agentjail policy add ~/my_policy.rego
Then confirm agentjail sees it:
agentjail policy list
You should see custom/my_policy/no-env-access alongside the built-in rules.
If the daemon is not running when you save the file, it will pick it up on next
start. You can also drop the file directly into ~/.agentjail/rules/ and
SIGHUP the daemon manually (skips validation):
cp ~/my_policy.rego ~/.agentjail/rules/
kill -HUP $(pgrep agentjail-daemon)
Test it
The fastest end-to-end check is agentjail try:
agentjail try "cat .env" # expect: DENY
agentjail try "cat README.md" # expect: ALLOW
Or pipe a raw PreToolUse JSON payload directly to the hook (daemon must be running):
Deny case: confirm the rule fires.
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"cat .env"}}' \
| agentjail-hook
Expected output:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked: command reads or modifies .env file"}}
Exit code is 2 on deny.
Allow case: confirm a harmless call passes through.
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo hello"}}' \
| agentjail-hook
Expected: exit code 0, permissionDecision is "allow" or "ask".
For more on writing test cases, see Testing policies.
For the full set of fields available in input, see The input schema.
For the evaluation model (verdict semantics), see The policy model.