Skip to main content

Coding Assistant Hooks

Supported modes: Hosted Hybrid Local Available in: Free Solo Teams (org-wide enrollment: Teams)

When NOT to use this

If you want to govern an LLM API call from your own application code, use the SDK or gateway. Hooks govern the AI assistant's tool calls (Bash, Read, Write) inside Claude Code / Cursor / Codex CLI -- they do NOT govern API requests to the LLM provider.

Coding assistant hooks let you govern AI coding tools without changing how your developers work. Install a hook on Claude Code, Gemini CLI, or Codex CLI, and every tool call the agent makes is evaluated against your policy before it executes. No code changes, no proxy, no signup required for local mode.

What Hooks Do

When a coding assistant invokes a tool (run a shell command, write a file, call an API), the hook intercepts the call and:

  1. Evaluates the tool name against your policy rules (first-match-wins, fail-closed default).
  2. Scans tool arguments for DLP violations (PII, secrets, proprietary patterns).
  3. Blocks denied calls and returns a branded [Control Zero] reason to the agent.
  4. Logs every decision (allow or deny) to a local audit file with full context.
  5. Optionally syncs audit data to your Control Zero backend when the machine is enrolled.
Developer uses Claude Code / Gemini CLI / Codex CLI
|
v
Tool call triggered (e.g., Bash, write_file, shell)
|
v
controlzero hook-check (stdin: tool call JSON)
|
+---> Policy evaluation (first-match-wins)
| |
| +---> ALLOW -> exit 0
| | stderr: [Control Zero] Allowed: <tool>
| |
| +---> DENY -> exit 2
| stdout: JSON block decision (Claude Code)
| stderr: [Control Zero] <reason> (Gemini/Codex)
|
+---> DLP scan on tool arguments
| |
| +---> PII/secret found + action=block -> DENY
|
+---> Tamper check (if policy is signed)
| |
| +---> HMAC mismatch -> flag in audit trail
|
+---> Audit log entry written (~/.controlzero/audit.log)

Supported Agents

AgentHook EventConfig LocationStatus
Claude CodePreToolUse~/.claude/settings.jsonAvailable
Gemini CLIBeforeTool~/.gemini/settings.jsonAvailable
Codex CLIPreToolUse~/.codex/hooks.jsonAvailable

Quick Start

Install the Control Zero CLI (requires Python 3.10+):

pip install controlzero

Install the hook for your coding assistant:

# Claude Code
controlzero install claude-code

# Gemini CLI
controlzero install gemini-cli

# Codex CLI
controlzero install codex-cli

Each command does three things:

  1. Writes a default policy to ~/.controlzero/policy.yaml (ALLOW ALL by default).
  2. Registers controlzero hook-check as a hook in the agent's config file.
  3. Confirms installation with a [Control Zero] Installed successfully message.

The default policy allows every tool call and logs it. Add deny rules to start blocking.

Optionally sign your policy for tamper detection:

controlzero sign-policy

Policy Format

Policies live at ~/.controlzero/policy.yaml. Rules are evaluated top-to-bottom, first match wins.

version: '1'

# Optional: DLP rules scan tool arguments for sensitive data
dlp_rules:
- id: block-credit-cards
pattern: '\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b'
category: pii
action: block
description: Credit card number

- id: block-korean-rrn
pattern: '\d{6}-[1-4]\d{6}'
category: pii
action: block
description: Korean Resident Registration Number

rules:
# Deny destructive shell commands (Claude Code)
- id: deny-bash
deny: 'Bash'
reason: 'Shell access requires approval. Use a file tool instead.'

# Deny shell in Gemini CLI
- id: deny-gemini-shell
deny: 'run_shell_command'
reason: 'Shell commands are blocked by policy.'

# Deny shell in Codex CLI
- id: deny-codex-shell
deny: 'shell'
reason: 'Shell commands are blocked by policy.'

# Allow everything else (remove for deny-by-default)
- id: allow-everything-else
allow: '*'
reason: 'Default allow.'

Pattern Syntax

  • * matches any tool name.
  • Bash matches only the tool named Bash (case-sensitive).
  • mcp__* matches all MCP tools.
  • mcp__github__* matches all tools from the github MCP server.
  • delete_* matches any tool starting with delete_.

Deny-by-Default

For regulated environments, remove the final allow: '*' rule. The evaluator fails closed: any tool call that does not match an explicit allow rule is denied.

DLP Rules

DLP rules scan tool call arguments (file content, command text, etc.) for sensitive patterns. Each rule specifies:

  • id: Unique identifier
  • pattern: Regex pattern to match
  • category: Classification (pii, financial, proprietary)
  • action: What to do on match (block, detect, mask)
  • description: Human-readable explanation

Built-in patterns are always active and include credit cards, SSNs, emails, API keys, and Korean locale patterns (RRN with mod11 validation, mobile phones, business IDs).

Per-Agent Details

Claude Code (PreToolUse)

The installer registers a PreToolUse hook in ~/.claude/settings.json:

{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "controlzero hook-check",
"timeout": 5000
}
]
}
]
}
}

Claude Code passes a JSON object on stdin with tool_name and tool_input. Common tool names: Bash, Read, Edit, Write, Glob, Grep, WebFetch.

MCP tools appear as mcp__<server>__<tool> (for example, mcp__github__create_issue).

On deny, Claude Code reads the JSON from stdout and displays the [Control Zero] branded reason to the user.

Gemini CLI (BeforeTool)

The installer registers a BeforeTool hook in ~/.gemini/settings.json:

{
"hooks": {
"BeforeTool": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "controlzero hook-check",
"timeout": 5000
}
]
}
]
}
}

Common Gemini CLI tool names: run_shell_command, read_file, write_file, edit_file, glob, search_file_content, web_fetch, google_web_search. MCP tools use the same mcp__<server>__<tool> naming convention.

On deny, Gemini CLI reads the rejection reason from stderr (exit code 2).

Codex CLI (PreToolUse via hooks.json)

The installer creates ~/.codex/hooks.json and enables hooks in ~/.codex/config.toml:

~/.codex/hooks.json:

{
"hooks": {
"PreToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "controlzero hook-check",
"timeout": 5000
}
]
}
]
}
}

~/.codex/config.toml (auto-added):

[features]
codex_hooks = true

Common Codex CLI tool names: Bash, shell, apply_patch. On deny, Codex reads the rejection reason from stderr (exit code 2).

Policy signing and tamper detection

Tamper detection catches the case where someone (or a compromised process) modifies your policy file after you wrote it. Without signing, an attacker could replace your strict policy with allow: * and your agent would silently lose all guardrails.

How signing works depends on whether you're a Control Zero user with an account, or running fully local without one. Both options give you tamper protection. The difference is who does the signing.

Hosted Signed automatically by Control Zero

If you're signed up at app.controlzero.ai and using an API key, we sign your policy bundle for you. Every time you save a policy in the dashboard, our backend signs the bundle before the SDK pulls it. You don't run sign-policy. You don't manage signing keys. The hook verifies the signature automatically on every tool call.

This is the default for free accounts and all paid tiers. Just sign up, write your policy in the dashboard, and tamper protection is on.

# Just install the hook with your API key. No signing step needed.
controlzero install claude-code --api-key cz_live_your_key_here

Local Self-sign for fully offline / no-account usage

If you don't want to sign up, or you're running in an air-gapped environment with a local policy file, you can sign the policy yourself. This gives the same tamper guarantee with zero dependency on Control Zero's backend.

# Sign the policy in your current directory
controlzero sign-policy

# Sign a specific file
controlzero sign-policy --policy /path/to/policy.yaml

# Verify a signature without re-signing
controlzero sign-policy --verify-only

The signing key is generated on first use and stored at ~/.controlzero/tamper.key (mode 0600, owner-readable only). Re-run controlzero sign-policy after every legitimate policy edit.

What tamper detection does

Whichever path you take, the hook verifies the signature on every tool call. If verification fails:

  • The audit log records tamper_detected: true
  • A warning appears in stderr: controlzero: policy file tamper detected
  • The configured tamper_behavior decides what happens next: warn (allow, log only), deny (deny this call), deny-all (deny everything until the issue is resolved), or quarantine (lock the agent into a deny-all state until an admin clears it)

You set tamper_behavior in your policy file or in the dashboard.

Built-in DLP coverage

DLP scanning runs against every tool call. The hooks ship with 65 built-in patterns across four categories. All patterns are active by default.

CategoryWhat it catchesExamples
PIIPersonal identifiers across regionsUS SSN, Korean RRN, Japanese My Number, UK NI, German Steuer-ID, French NIR, Indian Aadhaar/PAN, Brazilian CPF, Canadian SIN, Australian TFN, Singapore NRIC, Hong Kong ID, email addresses, phone numbers (US/Korean/Japanese)
FinancialMoney-related identifiersCredit card numbers (with Luhn validation), IBAN, SWIFT/BIC, US routing numbers, Bitcoin and Ethereum addresses
HealthcareHIPAA-relevant identifiersUS NPI, US DEA registration numbers
SecretsAPI keys, tokens, connection stringsAWS, GCP, Azure, OpenAI, Anthropic, HuggingFace, Cohere, GitHub PAT, GitLab PAT, npm, PyPI, Stripe (publishable + secret), SendGrid, Slack tokens + webhooks, Datadog, Sentry, Linear, New Relic, Vault, Doppler, JWT, SSH private keys, PEM certificates, bearer tokens, Postgres / MySQL / MongoDB / Redis / RabbitMQ connection strings, Control Zero API keys

See Locale-aware DLP for the complete pattern list with regex shapes.

Custom DLP rules

Add organization-specific patterns (internal project codes, proprietary identifiers, internal hostnames) via the dashboard's DLP Rules Editor, or directly in your policy file:

dlp_rules:
- id: internal-project-code
pattern: 'PROJ-[A-Z]{3}-\d{6}'
category: custom
action: block
reason: 'Internal project code -- do not paste into external tools'

- id: customer-account-id
pattern: 'CUST_[0-9]{10}'
category: pii
action: mask
reason: 'Customer account IDs are masked in audit logs'

- id: internal-jira-ticket
pattern: '\bACME-\d{4,6}\b'
category: custom
action: detect
reason: 'Track which agents reference internal tickets'

Three actions:

  • block — the call is denied. The tool never executes.
  • mask — the matched string is replaced with ***** before the call proceeds. The agent gets a redacted version.
  • detect — the call proceeds unchanged. The match is recorded in audit for visibility.

Patterns use Python re syntax (PCRE-compatible subset). Test new patterns from the dashboard before publishing -- the editor runs them against sample text live.

Audit Log

Every tool call is logged to ~/.controlzero/audit.log as JSON lines:

{
"ts": "2026-04-12T07:30:48.429Z",
"decision": "deny",
"tool": "Bash",
"method": "*",
"policy_id": "deny-bash",
"reason": "Shell access requires approval. Use a file tool instead.",
"args_keys": ["command"],
"mode": "local",
"dlp_findings": [],
"tamper_detected": false,
"audit_chain_broken": false
}

View recent audit entries:

# Last 10 entries
tail -10 ~/.controlzero/audit.log

# Filter for denials
grep '"deny"' ~/.controlzero/audit.log

# Filter for DLP blocks
grep 'dlp_findings' ~/.controlzero/audit.log | grep -v '\\[\\]'

# Filter for tamper events
grep '"tamper_detected": true' ~/.controlzero/audit.log

# Watch live
tail -f ~/.controlzero/audit.log

The audit log rotates daily with 30-day retention.

Enterprise Enrollment

Org-wide enrollment: Teams -- Individual hooks work on all tiers. Fleet enrollment, centralized policy management, and audit log aggregation across developers are available in Teams. View pricing

For fleet-wide governance, enroll machines with the Control Zero backend:

controlzero install claude-code --api-key cz_live_your_key_here

Once enrolled:

  • Policies sync from the dashboard to ~/.controlzero/policy.yaml automatically.
  • Audit data ships to the backend for centralized visibility.
  • DLP rules from the dashboard (including locale patterns) are enforced locally.
  • Fleet status appears on the Devices page in the dashboard.

The hook continues to work locally even when the backend is unreachable. Audit entries queue locally and sync when connectivity resumes.

Verify Installation

# Claude Code: check hook is registered
cat ~/.claude/settings.json | python3 -m json.tool | grep -A6 PreToolUse

# Gemini CLI: check hook is registered
cat ~/.gemini/settings.json | python3 -m json.tool | grep -A8 BeforeTool

# Codex CLI: check hooks.json
cat ~/.codex/hooks.json | python3 -m json.tool | grep -A8 PreToolUse

# Verify policy signature
controlzero sign-policy --verify-only

# Smoke test the evaluator (should show [Control Zero] Allowed)
echo '{"tool_name":"Read","tool_input":{"file":"/tmp/test"}}' \
| controlzero hook-check

# Tail the audit log
tail -f ~/.controlzero/audit.log

Troubleshooting

Hook is not firing. Confirm controlzero is on the PATH for the user running the agent. If you installed in a venv, the agent may not inherit that PATH.

Policy change not taking effect. The hook reloads the policy on every tool call. Check that no ./controlzero.yaml in the current project directory is shadowing the global file. Per-project policies always win over the global one.

Audit log is missing. Check permissions on ~/.controlzero/. The directory must be writable by the user running the agent.

Gemini CLI not blocking denied tools. Verify the BeforeTool hook uses the nested format (with an inner hooks array). The flat format from earlier versions is not recognized by Gemini CLI. Run controlzero install gemini-cli --force to update.

Codex CLI not blocking. Verify ~/.codex/hooks.json exists (not the legacy config.toml wrapper). Run controlzero install codex-cli --force to update. Ensure codex_hooks = true is set in ~/.codex/config.toml.

Tamper detection showing false positives. If you edited the policy legitimately, re-sign it: controlzero sign-policy. If you did not edit it, investigate. Check file permissions and modification timestamps.