Documentation
Govern what an agent can do, not just what it says — the tools it offers the model, the MCP servers wired into the session, the skills it exposes, and which client is driving.
Guardrails govern what a request says. This section governs what it can do.
Every request an AI coding agent makes carries a list of the tools it is offering
the model: its own (Bash, Read, Edit), the flattened tools of every MCP
server the developer has wired in, and any remote MCP server the provider is being
asked to call. These APIs are stateless, so the client re-sends that whole list on
every turn. The firewall reads it, can refuse it, and records it.
That closes a gap most organisations do not know they have. A developer adds an MCP server to a local config on a Tuesday. It holds a token for the issue tracker. It is now in every prompt that developer’s agent sends, and it appears in no manifest, no repository scan, and no change ticket.
| Family | Rule types | Matches against |
|---|---|---|
| Tools | tool_allow, tool_deny | Every tool name in the request, including MCP tools the client has flattened in |
| MCP servers | mcp_allow, mcp_deny | Remote MCP servers the request declares by URL |
| Skills | skill_allow, skill_deny | Named skills offered through a dispatching tool |
| Clients | client_allow, client_deny | The calling agent, derived from request headers |
| Action | What happens |
|---|---|
flag | The request proceeds. The match is recorded. |
strip | The tool or MCP server is removed from the request and the request proceeds. The model is never offered it. Tools and MCP servers only. |
block | The request is rejected with a 403, naming the rule. |
redact is a content action and does not apply here — there is no text to rewrite.
strip is its analogue on this plane.
flag, and start from the inventory. You
almost certainly do not yet know which tools your developers carry. A rule written
from a guess breaks a working session; a rule written from a list of what is
actually running does not. See rolling out a rule.Content guardrails take an RE2 regex. Capability rules do not — they take a glob,
where only * is special and every other character is literal.
| Pattern | Matches | Does not match |
|---|---|---|
Bash | Bash | Bashful, bash, BashOutput |
mcp__github__* | every tool from the github MCP server | mcp__gitlab__create_issue |
*__delete_* | mcp__jira__delete_issue | mcp__jira__create_issue |
^Bash$ | a tool literally named ^Bash$ | Bash |
A name list is the one place regex power is a liability. Someone typing a tool name
writes Bash and means exactly Bash; an unanchored regex would also match
Bashful, and a . typed by habit would match any character. Matching is
case-sensitive, because wire names are.
See the glob reference.
Any enabled *_allow rule switches its family into allowlist mode: from then
on, anything in that family not matched by an allow rule is refused.
This is the same sentence as the model allow/deny lists, deliberately. One rule to learn, not two.
If several allow rules in a family disagree about the action, the strictest one governs what happens to an unlisted capability. Adding a permissive rule can never weaken enforcement an existing one already provides.
priority, then allowlist gaps. A deny you wrote by hand always reports itself
as the cause rather than being masked by “not in the allowlist”.block short-circuits.priority therefore orders rules within a plane, not across the two. A content
rule at priority 1 still runs after a capability rule at priority 999.
HTTP 403, in the dialect of whatever client asked, with a code naming the
family so a caller can branch on it:
{
"type": "error",
"error": {
"type": "permission_error",
"message": "request blocked by AI firewall policy: tool \"Bash\" denied by policy \"no-shell\"",
"code": "tool_denied",
"blocked_by": "no-shell",
"violations": [
{
"policy_name": "no-shell",
"rule_type": "tool_deny",
"action": "block",
"detail": "tool \"Bash\" denied by policy \"no-shell\""
}
]
}
}
Codes: tool_denied, mcp_denied, skill_denied, client_denied. The full list
is in the error reference.
The Tools & MCP tab of the AI Firewall dashboard.
Start at the inventory panel: it lists what your agents actually carry, and each
row has a one-click Deny or Allow that writes an ordinary rule set to
flag.
vulnetix ai-firewall policy guardrail no-jira-writes \
--rule-type tool_deny \
--action strip \
--pattern 'mcp__jira__*' \
--priority 100 \
--enable
Read the inventory first:
vulnetix ai-firewall inventory --kind mcp_server
# .vulnetix/ai-firewall.yaml
apiVersion: vulnetix.com/v1
kind: AiFirewallPolicy
spec:
guardrails:
- name: no-jira-writes
ruleType: tool_deny
action: strip
pattern: 'mcp__jira__*'
priority: 100
enabled: true
See policy as code.
Stated here rather than only at the end, because it changes what the feature is for:
A tool runs on the developer’s machine before the firewall ever sees it. When
Claude Code runs Bash, it runs locally, and the result is what gets sent. A
rule can stop the tool being offered so the model never calls it again, and can
refuse a request whose history shows it was called — but it cannot un-run a
command.
Client identity comes from headers, which any caller can set. It is attribution, and it is genuinely useful for that. It is not a security boundary. Client identity says so at the top of the page.
The body-side controls are the ones with teeth, and not because a body cannot be faked either: a lie there is self-defeating. A client that hides a tool from the firewall has also hidden it from the model.