Canonical Tool Names
Control Zero policies use canonical tool names so one rule covers every supported AI client. The Control Zero SDK normalises every host-agent tool name (run_shell_command, Bash, shell, PowerShell, ...) to its canonical equivalent (Bash) before the policy engine evaluates anything. This page is the source of truth for which canonical names exist, which client tools each one covers, and how to add a new client.
TL;DR
Write rules using the canonical name. The SDK takes care of mapping each client's tool taxonomy to the canonical form.
# Single rule, covers Claude Code + Gemini CLI + Codex CLI + PowerShell on Windows
- effect: deny
action: Bash:rm
message: rm blocked in production
Canonical tools
| Canonical | Claude Code | Gemini CLI | Codex CLI | What it covers |
|---|---|---|---|---|
Bash | Bash | run_shell_command | shell | Any shell-command execution. PowerShell aliases here too. |
file_read | Read | read_file, read_many_files | (sandbox-gated) | Reading file contents. |
file_write | Write, Edit | write_file, replace, edit_file | apply_patch | Writing or editing file contents. |
file_search | Grep, Glob | grep_search, glob | (n/a) | Pattern-matching file discovery. |
web_search | WebSearch | google_web_search | (n/a) | Search engine queries. |
http | WebFetch | web_fetch | (n/a) | Generic HTTP requests. |
browser | (via MCP) | (via MCP) | (via MCP) | Headless browser automation. |
database | (via MCP) | (via MCP) | (via MCP) | SQL execution. |
task | Task, Agent, Skill | (n/a) | (n/a) | Subagent / nested-agent invocation. |
The SDK appends a :method suffix to each canonical name when it can extract one from arguments (e.g. the Bash command argument rm -rf / becomes action Bash:rm; a SQL DROP TABLE becomes database:drop). If no method can be extracted, the action is <canonical>:*.
Writing portable rules
Use the canonical name plus a method when you want to be specific:
| Goal | Rule |
|---|---|
| Block destructive shell rm anywhere | action: Bash:rm |
| Block any shell command (last resort) | action: Bash (= Bash:*) |
| Block writes to .env files | action: file_write, condition args.path matches '.env' |
| Allow only read-only DB access | effect: allow, action: database:select then effect: deny, action: database |
* and *:* match anything. tool:* matches any method on that tool.
How the normalisation runs
- The host CLI (Claude Code, Gemini CLI, Codex CLI, MCP server, etc.) emits a PreToolUse / equivalent event with a tool name and arguments.
- The Control Zero SDK hook reads
sdks/python/maruthiprithivi/control_zero/_internal/tool_extractors.json(orsdks/node/controlzero/src/internal/toolExtractors.jsonfor Node). - The hook resolves the host tool name through the alias map to a canonical name.
- The hook walks
args_pathand appliesextractto derive a method (if any). - The policy engine evaluates rules against
{canonical_tool}:{method}.
The SDK is the single normalisation point. The dashboard, Test panel, blueprint library, and docs all use canonical names so what you author is what fires.
SQL semantic classes
For the database canonical tool, the SDK derives BOTH a per-keyword method (database:SELECT, database:DROP, ...) and a portable canonical semantic class (database:read, database:write, database:admin, database:exec). A rule that targets the class fires for any statement whose first keyword belongs to that class -- regardless of which dialect-specific spelling the agent emitted.
| Class | Covers | Examples |
|---|---|---|
database:read | SELECT, EXPLAIN (read variants), SHOW, DESCRIBE, USE, WITH (CTE that produces a SELECT), VALUES | SELECT * FROM users, WITH x AS (SELECT 1) SELECT * FROM x, EXPLAIN SELECT ..., SHOW TABLES |
database:write | INSERT, UPDATE, DELETE, MERGE, UPSERT, REPLACE, COPY (table target), LOAD | INSERT INTO ..., UPDATE x SET ..., DELETE FROM x WHERE ..., MERGE INTO target USING source ... |
database:admin | CREATE, ALTER, DROP, TRUNCATE, GRANT, REVOKE, RENAME, ATTACH, DETACH, REINDEX, VACUUM, ANALYZE, OPTIMIZE, CLUSTER, LOCK | CREATE TABLE ..., ALTER TABLE ..., DROP TABLE ..., GRANT SELECT ON ... TO ... |
database:exec | CALL, EXECUTE, DO, BEGIN, COMMIT, ROLLBACK, START, SAVEPOINT (transaction control + procedures) | BEGIN, COMMIT, ROLLBACK, CALL my_proc() |
Multi-statement: most-dangerous-class wins. A SQL-injection piggyback like SELECT 1; DROP TABLE users resolves to database:admin, so a deny: database:admin rule catches it even though the leading statement is a SELECT. The class ordering is admin > write > exec > read.
Rules can target either layer. The policy engine matches a rule against BOTH the per-keyword action AND the class action, so:
- A rule with
action: database:readallows SELECT, EXPLAIN, SHOW, DESCRIBE, and CTE in one line. - A rule with
action: database:DROPcontinues to match DROP specifically and leaves CREATE / ALTER / TRUNCATE alone.
Portable read-only blueprint:
- id: read-only-db
effect: allow
action: database:read
reason: Reads of any kind are permitted.
- id: deny-write
effect: deny
action: database:write
- id: deny-admin
effect: deny
action: database:admin
- id: deny-exec
effect: deny
action: database:exec
The semantic class covers SELECT, EXPLAIN, SHOW, CTE, etc. and is independent of the dialect's keyword spelling.
When to use raw keywords vs the class:
- Use the class for portable, intent-level rules (
allow database:read,deny database:admin). - Use the keyword when you want fine-grained control (
deny database:DROPwhile still allowingdatabase:CREATE).
MCP tools
MCP server tools are passed through unchanged because their names are dynamic (mcp__<server>__<tool>). Match them with their literal name or a glob:
- effect: deny
action: mcp__github__delete_repo
- effect: deny
action: mcp__db__*
Adding a new client (SDK contributors)
When wiring in a new platform — e.g. a new IDE plugin, a new CLI, or a new MCP server — apply the same alias contract end-to-end:
- Add the new client's tool names as
aliasesunder the matching canonical entry insdks/python/maruthiprithivi/control_zero/_internal/tool_extractors.json. Add a NEW canonical entry only if the client's tool concept does not map to any existing canonical (rare). - Mirror the change byte-for-byte to
sdks/node/controlzero/src/internal/toolExtractors.json. The CI parity test fails if they drift. - If the new client emits arguments in a different shape, add a new
extractfunction inhook_extractors.py(and the Node sibling) and reference it from the JSON. - Add a row to the table at the top of this doc.
- Add fixtures to
tests/fixtures/enforcement-spec/extractors/parity-cases.jsoncovering the new client. The cross-SDK parity harness asserts Python and Node produce the same canonical action for each fixture input. - Bump
spec_versionin both JSONs if the contract change is breaking (a new alias is additive — bump only when removing one or changing a canonical name).
Audit log
The audit log carries BOTH the canonical tool name and the raw host tool name in every row (tool = canonical, host_tool_name = raw). When debugging "why didn't my rule match", check host_tool_name to see what the SDK actually saw, and extracted_action to see the canonical action it was evaluated against.
Testing your policy
Use the dashboard Test panel (Policy Builder → Test). Enter the canonical name as the Tool Name. Sample inputs:
Bash+ Input Text:rm -rf /→ matches aBash:rmdeny ruleBash+ Input Text:kubectl get pods→ matchesBash:kubectlif you have oneRead+ Input Text: any → matches afile_readrule
The Test panel runs the same evaluator as production, so what you see is what fires.
Audit log argument redaction
By default, only the names of the arguments to each governed tool call are persisted in the audit log -- values are dropped at the SDK boundary. If your compliance posture needs argument-level visibility with an audit-on-decrypt chain, see Audit log argument redaction for the three storage modes and how to opt in.