Skip to main content

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

CanonicalClaude CodeGemini CLICodex CLIWhat it covers
BashBashrun_shell_commandshellAny shell-command execution. PowerShell aliases here too.
file_readReadread_file, read_many_files(sandbox-gated)Reading file contents.
file_writeWrite, Editwrite_file, replace, edit_fileapply_patchWriting or editing file contents.
file_searchGrep, Globgrep_search, glob(n/a)Pattern-matching file discovery.
web_searchWebSearchgoogle_web_search(n/a)Search engine queries.
httpWebFetchweb_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.
taskTask, 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:

GoalRule
Block destructive shell rm anywhereaction: Bash:rm
Block any shell command (last resort)action: Bash (= Bash:*)
Block writes to .env filesaction: file_write, condition args.path matches '.env'
Allow only read-only DB accesseffect: allow, action: database:select then effect: deny, action: database

* and *:* match anything. tool:* matches any method on that tool.

How the normalisation runs

  1. The host CLI (Claude Code, Gemini CLI, Codex CLI, MCP server, etc.) emits a PreToolUse / equivalent event with a tool name and arguments.
  2. The Control Zero SDK hook reads sdks/python/maruthiprithivi/control_zero/_internal/tool_extractors.json (or sdks/node/controlzero/src/internal/toolExtractors.json for Node).
  3. The hook resolves the host tool name through the alias map to a canonical name.
  4. The hook walks args_path and applies extract to derive a method (if any).
  5. 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.

ClassCoversExamples
database:readSELECT, EXPLAIN (read variants), SHOW, DESCRIBE, USE, WITH (CTE that produces a SELECT), VALUESSELECT * FROM users, WITH x AS (SELECT 1) SELECT * FROM x, EXPLAIN SELECT ..., SHOW TABLES
database:writeINSERT, UPDATE, DELETE, MERGE, UPSERT, REPLACE, COPY (table target), LOADINSERT INTO ..., UPDATE x SET ..., DELETE FROM x WHERE ..., MERGE INTO target USING source ...
database:adminCREATE, ALTER, DROP, TRUNCATE, GRANT, REVOKE, RENAME, ATTACH, DETACH, REINDEX, VACUUM, ANALYZE, OPTIMIZE, CLUSTER, LOCKCREATE TABLE ..., ALTER TABLE ..., DROP TABLE ..., GRANT SELECT ON ... TO ...
database:execCALL, 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:read allows SELECT, EXPLAIN, SHOW, DESCRIBE, and CTE in one line.
  • A rule with action: database:DROP continues 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:DROP while still allowing database: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:

  1. Add the new client's tool names as aliases under the matching canonical entry in sdks/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).
  2. Mirror the change byte-for-byte to sdks/node/controlzero/src/internal/toolExtractors.json. The CI parity test fails if they drift.
  3. If the new client emits arguments in a different shape, add a new extract function in hook_extractors.py (and the Node sibling) and reference it from the JSON.
  4. Add a row to the table at the top of this doc.
  5. Add fixtures to tests/fixtures/enforcement-spec/extractors/parity-cases.json covering the new client. The cross-SDK parity harness asserts Python and Node produce the same canonical action for each fixture input.
  6. Bump spec_version in 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 a Bash:rm deny rule
  • Bash + Input Text: kubectl get pods → matches Bash:kubectl if you have one
  • Read + Input Text: any → matches a file_read rule

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.