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.
Bash
For the Bash tool, tool_input contains:
| Field | Type | Description |
|---|---|---|
command | string | The shell command string the agent wants to run. |
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:
deny[msg] {
input.tool_name == "Write"
contains(input.tool_input.file_path, "/.ssh/")
msg := "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:
deny[msg] {
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")
msg := "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
deny[msg] {
input.tool_name == "Bash"
contains(input.tool_input.command, "curl")
not contains(input.tool_input.command, "api.yourcompany.com")
msg := "Blocked: curl to an unexpected host"
}
# Block git push with --force
deny[msg] {
input.tool_name == "Bash"
contains(input.tool_input.command, "git push")
contains(input.tool_input.command, "--force")
msg := "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:
deny[msg] {
input.tool_name == "Bash"
contains(input.tool_input.command, "secret")
msg := "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.