Every rule receives the same input object describing the tool call the agent
is about to make. Understanding its shape is the starting point for writing any
rule.
Top-level fields
{
"hook_event": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf ~/.ssh/"
},
"session_id": "abc123",
"cwd": "/home/user/project"
}
Fields always present:
input.hook_event: the lifecycle event name (e.g."PreToolUse").input.tool_name: the name of the tool as a string (e.g."Bash").input.tool_input: an object whose keys depend on the tool being called.input.session_id: the current agent session identifier.input.cwd: the working directory the agent is operating in.
Fields present for Bash tool calls only:
input.command_binaries: an array of binary basenames extracted from the shell command by the daemon’s parser. For"git status && agentjail policy list | grep foo", this is["git", "agentjail", "grep"]. Use this instead of regex-matching the raw command string to identify which binary is being invoked.
Bash
For the Bash tool, tool_input contains:
| Field | Type | Description |
|---|---|---|
command | string | The shell command string the agent wants to run. |
command_binaries | string[] | Basenames of all command binaries in the pipeline/chain (daemon-enriched). |
Example:
{
"hook_event": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "cat ~/.ssh/id_rsa"
}
}
Rule fragment:
input.tool_name == "Bash"
input.tool_input.command # the command string
File tools
| Tool | Relevant tool_input field |
|---|---|
Write | file_path |
Edit | file_path |
Read | file_path (also accepted as path) |
Rename | old_path |
NotebookEdit | notebook_path |
Example rule blocking writes to sensitive paths:
candidate contains r if {
input.tool_name == "Write"
contains(input.tool_input.file_path, "/.ssh/")
r := {
"action": "deny",
"rule_id": "custom/my_policy/no-ssh-write",
"reason": "Blocked: write to ~/.ssh/ is not allowed",
}
}
MCP tools
MCP tool names follow the pattern mcp__<server>__<tool>. Match on
input.tool_name using startswith or regex.match:
candidate contains r if {
startswith(input.tool_name, "mcp__")
input.tool_input.url # field name varies per MCP server
not startswith(input.tool_input.url, "https://api.yourcompany.com")
r := {
"action": "deny",
"rule_id": "custom/my_policy/no-unexpected-mcp-url",
"reason": "Blocked: MCP request to unexpected URL",
}
}
The fields inside tool_input vary by MCP server. Common examples are path
and url, but check your server’s schema for the authoritative list.
Network and git operations
There is no dedicated network tool or git tool. Commands such as curl,
wget, and git are invoked through the Bash tool and appear as plain
strings in input.tool_input.command. Match them with string functions:
# Block curl to unknown hosts
candidate contains r if {
input.tool_name == "Bash"
contains(input.tool_input.command, "curl")
not contains(input.tool_input.command, "api.yourcompany.com")
r := {
"action": "deny",
"rule_id": "custom/my_policy/no-unexpected-curl",
"reason": "Blocked: curl to an unexpected host",
}
}
# Block git push with --force
candidate contains r if {
input.tool_name == "Bash"
contains(input.tool_input.command, "git push")
contains(input.tool_input.command, "--force")
r := {
"action": "deny",
"rule_id": "custom/my_policy/no-force-push",
"reason": "Blocked: force push is not allowed",
}
}
Inspecting a real call
To see the exact JSON a rule will receive, pipe a PreToolUse payload to the hook while the daemon is running:
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls -la"}}' \
| agentjail-hook
The hook prints the full hookSpecificOutput response, including the
permissionDecision and permissionDecisionReason. There is no separate
verbose or debug flag that dumps the raw input document.
Writing rules that are safe to fail
When a field you reference does not exist in tool_input, the Rego expression
is undefined and the rule does not fire. That means a rule like this:
candidate contains r if {
input.tool_name == "Bash"
contains(input.tool_input.command, "secret")
r := {
"action": "deny",
"rule_id": "custom/my_policy/no-secret-in-command",
"reason": "Blocked: possible secret in command",
}
}
will simply not fire for any non-Bash tool, because input.tool_name == "Bash"
will be false. You do not need a separate guard for every tool.
For practical rule examples built on this schema, see Rule recipes.