← All docs

Your first rule

Anatomy of a deny rule, how to write one that blocks a command, where to put it, and how to test it.

A policy is a Rego file with one or more deny rules. Each rule inspects an incoming tool call and fires when its conditions hold. If any rule fires, the call is blocked and the message is returned to the agent.

This page walks through writing a single rule from scratch, loading it, and confirming it works.

Anatomy of a deny rule

deny[msg] {
  input.tool_name == "Bash"
  contains(input.tool_input.command, "/.ssh/")
  msg := "Blocked: command targets sensitive path ~/.ssh/"
}

Breaking that down:

  • deny[msg] declares that this is a deny rule that produces a message.
  • The body is a conjunction: every line must hold for the rule to fire.
  • input.tool_name is the name of the tool the agent is trying to call.
  • input.tool_input holds the tool’s arguments. For a Bash call, input.tool_input.command is the shell command string.
  • msg is the string returned to the agent when the rule fires.

If any condition does not hold, the rule does not fire. agentjail only blocks the call if at least one rule produces a message.

See The input schema for the full shape of input and how fields vary by tool.

Write a rule

Place policy files in ~/.agentjail/rules/. The daemon loads every *.rego file in that directory (non-recursive) on startup and on reload.

Create a file called ~/.agentjail/rules/my_policy.rego:

package my_policy

deny[msg] {
  input.tool_name == "Bash"
  contains(input.tool_input.command, ".env")
  msg := "Blocked: command reads or modifies .env file"
}

The package name should match the file name by convention. The rule fires whenever a Bash command string contains .env.

Load and list policies

After saving the file, confirm agentjail sees it:

agentjail policy list

You should see my_policy alongside the built-in rules.

No daemon restart is needed. Running agentjail policy enable <name> or agentjail policy disable <name> sends SIGHUP and the daemon hot-reloads and recompiles all rules atomically. If the daemon was not running when you saved the file, it will pick it up on next start.

Test it

The fastest end-to-end test is to pipe a PreToolUse JSON payload directly to the hook (the 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".

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 (allow vs. deny semantics), see The policy model.