1"use strict";(globalThis.webpackChunkcontrolzero_docs||=[]).push([[7896],{44456(e,n,o){o.r(n),o.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>h,frontMatter:()=>i,metadata:()=>s,toc:()=>d});const s=JSON.parse('{"id":"guides/coding-assistant-hooks","title":"Coding Assistant Hooks","description":"Supported modes: Hosted Hybrid Local","source":"@site/docs/guides/coding-assistant-hooks.md","sourceDirName":"guides","slug":"/guides/coding-assistant-hooks","permalink":"/docs/guides/coding-assistant-hooks","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":10,"frontMatter":{"sidebar_position":10,"title":"Coding Assistant Hooks"},"sidebar":"docsSidebar","previous":{"title":"Gateway: zero-code API governance","permalink":"/docs/guides/gateway"},"next":{"title":"MCP server: let Claude manage your policies","permalink":"/docs/guides/mcp-server"}}');var r=o(74848),t=o(28453);const i={sidebar_position:10,title:"Coding Assistant Hooks"},l="Coding Assistant Hooks",a={},d=[{value:"What Hooks Do",id:"what-hooks-do",level:2},{value:"Supported Agents",id:"supported-agents",level:2},{value:"Quick Start",id:"quick-start",level:2},{value:"Policy Format",id:"policy-format",level:2},{value:"Pattern Syntax",id:"pattern-syntax",level:3},{value:"Deny-by-Default",id:"deny-by-default",level:3},{value:"DLP Rules",id:"dlp-rules",level:3},{value:"Per-Agent Details",id:"per-agent-details",level:2},{value:"Claude Code (PreToolUse)",id:"claude-code-pretooluse",level:3},{value:"Gemini CLI (BeforeTool)",id:"gemini-cli-beforetool",level:3},{value:"Codex CLI (PreToolUse via hooks.json)",id:"codex-cli-pretooluse-via-hooksjson",level:3},{value:"Policy signing and tamper detection",id:"policy-signing-and-tamper-detection",level:2},{value:'<span class="mode-pill hosted">Hosted</span> Signed automatically by Control Zero',id:"hosted-signed-automatically-by-control-zero",level:3},{value:'<span class="mode-pill local">Local</span> Self-sign for fully offline / no-account usage',id:"local-self-sign-for-fully-offline--no-account-usage",level:3},{value:"What tamper detection does",id:"what-tamper-detection-does",level:3},{value:"Built-in DLP coverage",id:"built-in-dlp-coverage",level:2},{value:"Custom DLP rules",id:"custom-dlp-rules",level:2},{value:"Audit Log",id:"audit-log",level:2},{value:"Enterprise Enrollment",id:"enterprise-enrollment",level:2},{value:"Verify Installation",id:"verify-installation",level:2},{value:"Troubleshooting",id:"troubleshooting",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components},{MaturityBadge:o}=n;return o||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("MaturityBadge",!0),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"coding-assistant-hooks",children:"Coding Assistant Hooks"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Supported modes:"})," ",(0,r.jsx)("span",{class:"mode-pill hosted",children:"Hosted"})," ",(0,r.jsx)("span",{class:"mode-pill hybrid",children:"Hybrid"})," ",(0,r.jsx)("span",{class:"mode-pill local",children:"Local"}),"\n",(0,r.jsx)(n.strong,{children:"Available in:"})," ",(0,r.jsx)("span",{class:"tier-pill free",children:"Free"})," ",(0,r.jsx)("span",{class:"tier-pill solo",children:"Solo"})," ",(0,r.jsx)("span",{class:"tier-pill teams",children:"Teams"})," (org-wide enrollment: ",(0,r.jsx)("span",{class:"tier-pill teams",children:"Teams"}),")"]}),"\n",(0,r.jsxs)(n.p,{children:["Coding assistant hooks provide deterministic enforcement on ",(0,r.jsx)(n.strong,{children:"Claude Code,\nGemini CLI, Cursor IDE, and Kiro CLI"}),". For every tool call covered by the\nhost's pre-execution hook, a deny stops the call before it executes. No code\nchanges, proxy, or signup is required for local mode."]}),"\n",(0,r.jsxs)(n.p,{children:["Coverage is declared per event, not per host: each audit row identifies the\nsurface and the enforcement coverage for that specific event. The\n",(0,r.jsx)(n.a,{href:"/docs/concepts/coverage",children:"capability matrix"})," is derived from the adapters' own\ncapability declarations. This page is written by hand; ",(0,r.jsx)(n.code,{children:"controlzero coverage --json"}),"\nexports those declarations for your installation and is the authority if the two\never disagree. A bare ",(0,r.jsx)(n.code,{children:"controlzero coverage"})," prints only a short per-host\nsummary.\nPolicy resolution fails closed by default: a missing policy bundle is denied\nrather than guessed at. (Age is not itself a deny trigger in the SDK -- a\ncached bundle with real rules keeps enforcing while a refresh is retried, which\nis the intended last-known-good behaviour.) A bundle that fails signature\nverification is never loaded: during a background refresh the agent keeps\nenforcing the last known good policy, and at startup there is nothing to fall\nback to, so calls are denied.\nCoverage is narrower on\nfour surfaces, each for a different reason: Codex CLI and Cursor CLI\n(",(0,r.jsx)(n.code,{children:"cursor-agent"}),") cover the ",(0,r.jsx)(n.code,{children:"execute"})," family only, and never receive the other\neight; on Antigravity the host may treat a deny as advisory in its headless\nmodes, so a denied call can still run there; Kiro IDE routes no tool call to a\ndeciding event, so it does not enforce. See\n",(0,r.jsx)(n.a,{href:"/docs/concepts/coverage",children:"Enforcement Coverage"})," for the per-tool detail."]}),"\n",(0,r.jsx)(n.admonition,{title:"Use the SDK or gateway for LLM API calls",type:"caution",children:(0,r.jsxs)(n.p,{children:["Hooks govern an assistant's tool calls (Bash, Read, Write), not requests to the\nLLM provider. For application-level LLM API calls, use the\n",(0,r.jsx)(n.a,{href:"/docs/sdk/python",children:"SDK"})," or ",(0,r.jsx)(n.a,{href:"/docs/guides/gateway",children:"gateway"}),"."]})}),"\n",(0,r.jsx)(n.h2,{id:"what-hooks-do",children:"What Hooks Do"}),"\n",(0,r.jsx)(n.p,{children:"When a coding assistant invokes a tool (run a shell command, write a file, call an API), the hook intercepts the call and:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Evaluates"})," the tool name against your policy rules (first-match-wins, fail-closed default)."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Scans"})," tool arguments for DLP violations (PII, secrets, proprietary patterns)."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Blocks"})," denied calls before execution and returns a branded ",(0,r.jsx)(n.code,{children:"[Control Zero]"})," reason to the agent."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Logs"})," every decision and its execution status to a complete audit trail."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Optionally syncs"})," audit data to your Control Zero backend when the machine is enrolled."]}),"\n"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Developer uses Claude Code / Gemini CLI / Codex CLI\n |\n v\n Tool call triggered (e.g., Bash, write_file, shell)\n |\n v\n controlzero hook-check (stdin: tool call JSON)\n |\n +---\x3e Policy evaluation (first-match-wins)\n | |\n | +---\x3e ALLOW -> exit 0\n | | stderr: [Control Zero] Allowed: <tool>\n | |\n | +---\x3e DENY -> exit 2\n | stdout: JSON block decision (Claude Code)\n | stderr: [Control Zero] <reason> (Gemini/Codex)\n |\n +---\x3e DLP scan on tool arguments\n | |\n | +---\x3e PII/secret found + action=block -> DENY\n |\n +---\x3e Tamper check (if policy is signed)\n | |\n | +---\x3e HMAC mismatch -> flag in audit trail\n |\n +---\x3e Audit log entry written (~/.controlzero/audit.log)\n"})}),"\n",(0,r.jsx)(n.h2,{id:"supported-agents",children:"Supported Agents"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Agent"}),(0,r.jsx)(n.th,{children:"Hook Event"}),(0,r.jsx)(n.th,{children:"Config Location"}),(0,r.jsx)(n.th,{children:"Status"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Claude Code"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"PreToolUse"}),", ",(0,r.jsx)(n.code,{children:"UserPromptSubmit"})]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"~/.claude/settings.json"})}),(0,r.jsxs)(n.td,{children:["A deny rule stops the call before it runs, across every tool family. One of only two hosts that can mask: through the Python SDK it honours ",(0,r.jsx)(n.code,{children:"hookSpecificOutput.updatedInput"}),", so a DLP ",(0,r.jsx)(n.code,{children:"mask"})," rule proceeds with redacted arguments"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Gemini CLI"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"BeforeTool"}),", ",(0,r.jsx)(n.code,{children:"BeforeAgent"})]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"~/.gemini/settings.json"})}),(0,r.jsxs)(n.td,{children:["A deny rule stops the call before it runs, across every tool family. One of only two hosts that can mask: through the Python SDK it honours ",(0,r.jsx)(n.code,{children:"hookSpecificOutput.tool_input"}),", so a DLP ",(0,r.jsx)(n.code,{children:"mask"})," rule proceeds with redacted arguments"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Codex CLI"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"PreToolUse"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"~/.codex/hooks.json"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.strong,{children:"Interactive TUI only."})," There, a deny rule stops a shell call before it runs. Shell only, and the cause is the host: Codex routes its shell tool to ",(0,r.jsx)(n.code,{children:"PreToolUse"}),", and a deny on an ",(0,r.jsx)(n.code,{children:"apply_patch"})," write or an MCP tool call is not imposed by Codex today (",(0,r.jsx)(n.a,{href:"https://github.com/openai/codex/issues/27833",children:"upstream openai/codex#27833"}),"). Headless ",(0,r.jsx)(n.code,{children:"codex exec"})," dispatches ",(0,r.jsx)(n.strong,{children:"no hooks at all"})," (",(0,r.jsx)(n.a,{href:"https://github.com/openai/codex/issues/26452",children:"upstream openai/codex#26452"}),"), so unattended and CI runs are neither enforced nor recorded. The other eight tool families are never delivered to Control Zero, so their absence from the audit log is not evidence they did not happen"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Cursor IDE"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"beforeShellExecution"}),", ",(0,r.jsx)(n.code,{children:"beforeMCPExecution"}),", ",(0,r.jsx)(n.code,{children:"beforeReadFile"}),", ",(0,r.jsx)(n.code,{children:"preToolUse"}),", ",(0,r.jsx)(n.code,{children:"subagentStart"}),", ",(0,r.jsx)(n.code,{children:"beforeSubmitPrompt"})]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"~/.cursor/hooks.json"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(o,{tier:"BETA"})," -- a deny rule stops the call before it runs, across every tool family, on six deciding events including ",(0,r.jsx)(n.code,{children:"subagentStart"}),". The install sets ",(0,r.jsx)(n.code,{children:"failClosed: true"}),", which governs how Cursor treats a hook that fails to run; it is a separate setting from the deny path, and on this surface an empty hook response means proceed. Cannot mask: a ",(0,r.jsx)(n.code,{children:"mask"})," rule escalates to a deny. The BETA is about our adapter, not that reach: it has not yet been revalidated against a current Cursor build"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Cursor CLI (",(0,r.jsx)(n.code,{children:"cursor-agent"}),")"]}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"beforeShellExecution"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"~/.cursor/hooks.json"})," (same install as Cursor IDE)"]}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(o,{tier:"BETA"})," -- a deny rule stops an ",(0,r.jsx)(n.code,{children:"execute"})," call before it runs. The install sets ",(0,r.jsx)(n.code,{children:"failClosed: true"}),", which governs how Cursor treats a hook that fails to run rather than the deny path itself; an empty hook response on this surface means proceed. Shell only: ",(0,r.jsx)(n.code,{children:"beforeShellExecution"})," is the one gate this surface reliably enforces, and the host does not deliver the other eight families to Control Zero. It never receives ",(0,r.jsx)(n.code,{children:"browser"}),", ",(0,r.jsx)(n.code,{children:"database"}),", ",(0,r.jsx)(n.code,{children:"file_read"}),", ",(0,r.jsx)(n.code,{children:"file_search"}),", ",(0,r.jsx)(n.code,{children:"file_write"}),", ",(0,r.jsx)(n.code,{children:"network"}),", ",(0,r.jsx)(n.code,{children:"prompt"})," or ",(0,r.jsx)(n.code,{children:"task"}),", so their absence from the audit log is not evidence they did not happen. The BETA is about our adapter: it has not yet been revalidated against a current Cursor build"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Kiro CLI (AWS)"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"preToolUse"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"~/.controlzero/policy.yaml"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(o,{tier:"GA",scope:"direct tool calls gated, and sub-agent spawns from SDK 1.13.12; a sub-agent's own tool calls only when it runs an agent config carrying our hooks"})," -- a deny rule stops direct tool calls before they run, across every tool family. A sub-agent's own tool calls reach ",(0,r.jsx)(n.code,{children:"preToolUse"})," and are blocked by a child exit 2 ",(0,r.jsx)(n.strong,{children:"when that sub-agent runs an agent config carrying our hooks"}),"; Kiro resolves hooks from the sub-agent's own config and does not inherit the parent's, so a sub-agent spawned without an explicit ",(0,r.jsx)(n.code,{children:"agent_name"})," runs its own tool calls ungated once it starts -- the spawn itself is still gated, the parent's hook fires either way -- and ",(0,r.jsx)(n.code,{children:"chat.defaultAgent"})," does not fill that in. Denying the ",(0,r.jsx)(n.code,{children:"use_subagent"})," spawn by rule needs ",(0,r.jsx)(n.strong,{children:"Python SDK 1.13.12 or later"}),": on any earlier SDK ",(0,r.jsx)(n.code,{children:"controlzero install kiro"})," never wires ",(0,r.jsx)(n.code,{children:"use_subagent"})," as a ",(0,r.jsx)(n.code,{children:"preToolUse"})," matcher, the hook is never invoked for that tool, and a ",(0,r.jsx)(n.code,{children:"deny"})," rule naming it silently never fires. Check ",(0,r.jsx)(n.code,{children:"controlzero --version"})," before relying on such a rule. That remaining gap is the host's (",(0,r.jsx)(n.a,{href:"https://github.com/kirodotdev/Kiro/issues/7755",children:"kirodotdev/Kiro#7755"}),", open) and is why the adapter still declares coverage ",(0,r.jsx)(n.code,{children:"partial"}),". ",(0,r.jsx)(n.code,{children:"use_subagent"})," is the only spawn tool this build exposes -- ",(0,r.jsx)(n.code,{children:"delegate"})," is disabled, ",(0,r.jsx)(n.code,{children:"agent_crew"})," is not exposed, and ",(0,r.jsx)(n.code,{children:"spawn_session"})," is a command value inside a session tool, so there are not four gateable spawn surfaces. Cannot mask: a ",(0,r.jsx)(n.code,{children:"mask"})," rule escalates to a deny. Driven against ",(0,r.jsx)(n.code,{children:"kiro-cli"})," 2.18.1"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Kiro IDE (AWS)"}),(0,r.jsx)(n.td,{children:"Prompt Submit, File Save"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:".kiro/hooks/"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(o,{tier:"COMING SOON"})," -- configured tier: ",(0,r.jsx)(n.code,{children:"observe"}),". Control Zero neither blocks nor records tool calls on this surface today, and ",(0,r.jsx)(n.strong,{children:"the gap is ours
1, not the vendor's"}),": current Kiro GA exposes a deterministic ",(0,r.jsx)(n.code,{children:"PreToolUse"})," gate that carries tool identity and arguments and blocks on exit code 2, and our hooks are not yet wired to it end to end. We say so rather than implying the host cannot do it, because a deterministic gate we are not plugged into is worth exactly zero enforcement, and describing that as a vendor limit would hide work that is on us. Declared coverage lifts when a block is observed end to end, not before. Declaration verified against Kiro IDE ",(0,r.jsx)(n.code,{children:">=0.12.333,<1.0.0"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Antigravity"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"PreToolUse"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"~/.gemini/config/hooks.json"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(o,{tier:"BETA"})," -- evaluates at ",(0,r.jsx)(n.code,{children:"PreToolUse"})," across every tool family and raises a deny. Our posture is fail-closed: empty stdout is read as a deny. Separately, and this is the host: Antigravity treats that deny as advisory in its headless / always-proceed modes, so on those modes the call may still run. Recorded is not stopped. The BETA is ours -- this adapter has not been revalidated against the shipping Antigravity build. See ",(0,r.jsx)(n.a,{href:"/docs/concepts/coverage",children:"Enforcement Coverage"})," for which Antigravity tools reach the hook. Install with ",(0,r.jsx)(n.code,{children:"controlzero install antigravity"})]})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["Install the Cursor, Kiro, and Antigravity hooks with ",(0,r.jsx)(n.code,{children:"controlzero"}),", the\n",(0,r.jsx)(n.strong,{children:"Python"})," CLI -- the ",(0,r.jsx)(n.code,{children:"controlzero install <agent>"})," syntax below is Python-only.\n",(0,r.jsx)(n.code,{children:"controlzero install kiro"})," installs deterministic\nenforcement for direct Kiro CLI tool calls: a deny rule stops the call before it\nruns, across every tool family. Kiro IDE is the separate boundary -- it routes\nno tool call to a deciding event, so it neither reports nor enforces on tool\ncalls. The older ",(0,r.jsx)(n.code,{children:"controlzero kiro init"})," remains as a deprecated alias -- see\nthe ",(0,r.jsx)(n.a,{href:"/docs/integrations/kiro",children:"Kiro integration page"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"quick-start",children:"Quick Start"}),"\n",(0,r.jsx)(n.p,{children:"Install the Control Zero CLI (requires Python 3.9+):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"pip install controlzero\n"})}),"\n",(0,r.jsx)(n.p,{children:"Install the hook for your coding assistant:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Claude Code\ncontrolzero install claude-code\n\n# Gemini CLI\ncontrolzero install gemini-cli\n\n# Codex CLI\ncontrolzero install codex-cli\n\n# Cursor\ncontrolzero install cursor\n\n# Kiro CLI\ncontrolzero install kiro\n\n# Antigravity\ncontrolzero install antigravity\n"})}),"\n",(0,r.jsx)(n.p,{children:"Each command does three things:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Writes a default policy to ",(0,r.jsx)(n.code,{children:"~/.controlzero/policy.yaml"})," (ALLOW ALL by default)."]}),"\n",(0,r.jsxs)(n.li,{children:["Registers the interpreter-pinned hook command in the agent's config file. The\ninstaller writes ",(0,r.jsx)(n.code,{children:'"<python>" -m controlzero hook-check'}),", where ",(0,r.jsx)(n.code,{children:"<python>"})," is the\nabsolute path to the interpreter that ran ",(0,r.jsx)(n.code,{children:"install"})," (so the value is\nmachine-specific), instead of a bare ",(0,r.jsx)(n.code,{children:"controlzero hook-check"})," that would resolve\nagainst the agent's ",(0,r.jsx)(n.code,{children:"PATH"})," at hook time (#1705/#1706)."]}),"\n",(0,r.jsxs)(n.li,{children:["Confirms installation with a ",(0,r.jsx)(n.code,{children:"[Control Zero] Installed successfully"})," message."]}),"\n"]}),"\n",(0,r.jsxs)(n.admonition,{type:"note",children:[(0,r.jsxs)(n.mdxAdmonitionTitle,{children:["Migrate a bare ",(0,r.jsx)(n.code,{children:"controlzero hook-check"})," by re-running install"]}),(0,r.jsxs)(n.p,{children:["If an agent's settings still show a bare ",(0,r.jsx)(n.code,{children:'"command": "controlzero hook-check"'})," (a\npre-#1705 install or a hand-edited config), re-run ",(0,r.jsx)(n.code,{children:"controlzero install <agent>"})," to\nmigrate it to the interpreter-pinned ",(0,r.jsx)(n.code,{children:'"<python>" -m controlzero hook-check'})," form.\n",(0,r.jsx)(n.code,{children:"controlzero doctor"})," flags a bare command on Claude Code, Gemini CLI, and Codex CLI.\nCursor and Kiro CLI were already pinned before #1705; Antigravity intentionally emits\nthe bare form."]})]}),"\n",(0,r.jsx)(n.p,{children:"The default policy allows every tool call and logs it. Add deny rules to start blocking."}),"\n",(0,r.jsx)(n.p,{children:"Hosted policy bundles are signed automatically and verified on every tool call.\nFor a fully local policy, enable the same tamper detection by signing it:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"controlzero sign-policy\n"})}),"\n",(0,r.jsx)(n.h2,{id:"policy-format",children:"Policy Format"}),"\n",(0,r.jsxs)(n.p,{children:["Policies live at ",(0,r.jsx)(n.code,{children:"~/.controlzero/policy.yaml"}),". Rules are evaluated top-to-bottom, first match wins."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"version: '1'\n\n# Optional: DLP rules scan tool arguments for sensitive data\ndlp_rules:\n - id: block-credit-cards\n pattern: '\\b\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}\\b'\n category: pii\n action: block\n description: Credit card number\n\n - id: block-korean-rrn\n pattern: '\\d{6}-[1-4]\\d{6}'\n category: pii\n action: block\n description: Korean Resident Registration Number\n\nrules:\n # Deny the shell on EVERY coding agent with ONE rule. Control Zero normalizes\n # each agent's shell tool to the canonical `Bash` (Claude `Bash`, Gemini\n # `run_shell_command`, Codex `shell`, Kiro `execute_bash`, Cursor `Shell`, ...),\n # so `bash:*` governs them all. Do NOT write a separate rule per agent against\n # the raw host tool name -- those names normalize to `Bash` and a raw-name rule\n # will not match the canonical action the agent emits.\n - id: deny-s
1hell\n deny: 'bash:*'\n reason: 'Shell access requires approval. Use a file tool instead.'\n\n # Allow everything else (remove for deny-by-default)\n - id: allow-everything-else\n allow: '*'\n reason: 'Default allow.'\n"})}),"\n",(0,r.jsx)(n.h3,{id:"pattern-syntax",children:"Pattern Syntax"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"*"})," matches any tool name."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"Bash"})," matches only the tool named ",(0,r.jsx)(n.code,{children:"Bash"})," (case-sensitive)."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"mcp__*"})," matches all MCP tools."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"mcp__github__*"})," matches all tools from the ",(0,r.jsx)(n.code,{children:"github"})," MCP server."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"delete_*"})," matches any tool starting with ",(0,r.jsx)(n.code,{children:"delete_"}),"."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"deny-by-default",children:"Deny-by-Default"}),"\n",(0,r.jsxs)(n.p,{children:["For regulated environments, remove the final ",(0,r.jsx)(n.code,{children:"allow: '*'"})," rule. The evaluator fails closed: any tool call that does not match an explicit ",(0,r.jsx)(n.code,{children:"allow"})," rule is denied."]}),"\n",(0,r.jsx)(n.h3,{id:"dlp-rules",children:"DLP Rules"}),"\n",(0,r.jsx)(n.p,{children:"DLP rules scan tool call arguments (file content, command text, etc.) for sensitive patterns. Each rule specifies:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"id"}),": Unique identifier"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"pattern"}),": Regex pattern to match"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"category"}),": Classification (pii, financial, proprietary)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"action"}),": What to do on match (block, detect, mask)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"description"}),": Human-readable explanation"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"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)."}),"\n",(0,r.jsx)(n.h2,{id:"per-agent-details",children:"Per-Agent Details"}),"\n",(0,r.jsx)(n.h3,{id:"claude-code-pretooluse",children:"Claude Code (PreToolUse)"}),"\n",(0,r.jsxs)(n.p,{children:["The installer registers a ",(0,r.jsx)(n.code,{children:"PreToolUse"})," hook in ",(0,r.jsx)(n.code,{children:"~/.claude/settings.json"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "hooks": {\n "PreToolUse": [\n {\n "matcher": "*",\n "hooks": [\n {\n "type": "command",\n "command": "\\"/usr/bin/python3\\" -m controlzero hook-check",\n "timeout": 5000\n }\n ]\n }\n ]\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Claude Code passes a JSON object on stdin with ",(0,r.jsx)(n.code,{children:"tool_name"})," and ",(0,r.jsx)(n.code,{children:"tool_input"}),". Common tool names: ",(0,r.jsx)(n.code,{children:"Bash"}),", ",(0,r.jsx)(n.code,{children:"Read"}),", ",(0,r.jsx)(n.code,{children:"Edit"}),", ",(0,r.jsx)(n.code,{children:"Write"}),", ",(0,r.jsx)(n.code,{children:"Glob"}),", ",(0,r.jsx)(n.code,{children:"Grep"}),", ",(0,r.jsx)(n.code,{children:"WebFetch"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["MCP tools appear as ",(0,r.jsx)(n.code,{children:"mcp__<server>__<tool>"})," (for example, ",(0,r.jsx)(n.code,{children:"mcp__github__create_issue"}),")."]}),"\n",(0,r.jsxs)(n.p,{children:["On deny, Claude Code reads the JSON from stdout and displays the ",(0,r.jsx)(n.code,{children:"[Control Zero]"})," branded reason to the user."]}),"\n",(0,r.jsx)(n.h3,{id:"gemini-cli-beforetool",children:"Gemini CLI (BeforeTool)"}),"\n",(0,r.jsxs)(n.p,{children:["The installer registers a ",(0,r.jsx)(n.code,{children:"BeforeTool"})," hook in ",(0,r.jsx)(n.code,{children:"~/.gemini/settings.json"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "hooks": {\n "BeforeTool": [\n {\n "matcher": ".*",\n "hooks": [\n {\n "type": "command",\n "command": "\\"/usr/bin/python3\\" -m controlzero hook-check",\n "timeout": 5000\n }\n ]\n }\n ]\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Common Gemini CLI tool names: ",(0,r.jsx)(n.code,{children:"run_shell_command"}),", ",(0,r.jsx)(n.code,{children:"read_file"}),", ",(0,r.jsx)(n.code,{children:"write_file"}),", ",(0,r.jsx)(n.code,{children:"edit_file"}),", ",(0,r.jsx)(n.code,{children:"glob"}),", ",(0,r.jsx)(n.code,{children:"search_file_content"}),", ",(0,r.jsx)(n.code,{children:"web_fetch"}),", ",(0,r.jsx)(n.code,{children:"google_web_search"}),". MCP tools use the same ",(0,r.jsx)(n.code,{children:"mcp__<server>__<tool>"})," naming convention."]}),"\n",(0,r.jsx)(n.p,{children:"On deny, Gemini CLI reads the rejection reason from stderr (exit code 2)."}),"\n",(0,r.jsx)(n.h3,{id:"codex-cli-pretooluse-via-hooksjson",children:"Codex CLI (PreToolUse via hooks.json)"}),"\n",(0,r.jsxs)(n.p,{children:["The installer creates ",(0,r.jsx)(n.code,{children:"~/.codex/hooks.json"})," and enables hooks in ",(0,r.jsx)(n.code,{children:"~/.codex/config.toml"}),":"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"~/.codex/hooks.json:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "hooks": {\n "PreToolUse": [\n {\n "matcher": ".*",\n "hooks": [\n {\n "type": "command",\n "command": "\\"/usr/bin/python3\\" -m controlzero hook-check",\n "timeout": 5000\n }\n ]\n }\n ]\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"~/.codex/config.toml"})," (auto-added):"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-toml",children:"[features]\ncodex_hooks = true\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Common Codex CLI tool names: ",(0,r.jsx)(n.code,{children:"Bash"}),", ",(0,r.jsx)(n.code,{children:"shell"}),", ",(0,r.jsx)(n.code,{children:"apply_patch"}),". On deny, Codex reads the rejection reason from stderr (exit code 2)."]}),"\n",(0,r.jsx)(n.h2,{id:"policy-signing-and-tamper-detection",children:"Policy signing and tamper detection"}),"\n",(0,r.jsxs)(n.p,{children:["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 ",(0,r.jsx)(n.code,{children:"allow: *"})," and your agent would silently lose all guardrails."]}),"\n",(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsxs)(n.h3,{id:"hosted-signed-automatically-by-control-zero",children:[(0,r.jsx)("span",{class:"mode-pill hosted",children:"Hosted"})," Signed automatically by Control Zero"]}),"\n",(0,r.jsxs)(n.p,{children:["If you're signed up at ",(0,r.jsx)(n.a,{href:"https://app.controlzero.ai",children:"app.controlzero.ai"})," and using an API key, ",(0,r.jsx)(n.strong,{children:"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 ",(0,r.jsx)(n.code,{children:"sign-policy"}),". You don't manage signing keys. The hook verifies the signature automatically on every tool call."]}),"\n",(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Just install the hook with your API key. No signing step needed.\ncontrolzero install claude-code --api-key cz_live_your_key_here\n"})}),"\n",(0,r.jsxs)(n.h3,{id:"local-self-sign-for-fully-offline--no-account-usage",children:[(0,r.jsx)("span",{class:"mode-pill local",children:"Local"}
1)," Self-sign for fully offline / no-account usage"]}),"\n",(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Sign the policy in your current directory\ncontrolzero sign-policy\n\n# Sign a specific file\ncontrolzero sign-policy --policy /path/to/policy.yaml\n\n# Verify a signature without re-signing\ncontrolzero sign-policy --verify-only\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The signing key is generated on first use and stored at ",(0,r.jsx)(n.code,{children:"~/.controlzero/tamper.key"})," (mode 0600, owner-readable only). Re-run ",(0,r.jsx)(n.code,{children:"controlzero sign-policy"})," after every legitimate policy edit."]}),"\n",(0,r.jsx)(n.h3,{id:"what-tamper-detection-does",children:"What tamper detection does"}),"\n",(0,r.jsx)(n.p,{children:"Whichever path you take, the hook verifies the signature on every tool call. If verification fails:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The audit log records ",(0,r.jsx)(n.code,{children:"tamper_detected: true"})]}),"\n",(0,r.jsxs)(n.li,{children:["A warning appears in stderr: ",(0,r.jsx)(n.code,{children:"controlzero: policy file tamper detected"})]}),"\n",(0,r.jsxs)(n.li,{children:["The configured ",(0,r.jsx)(n.code,{children:"tamper_behavior"})," decides what happens next: ",(0,r.jsx)(n.code,{children:"warn"})," (allow, log only), ",(0,r.jsx)(n.code,{children:"deny"})," (deny this call), ",(0,r.jsx)(n.code,{children:"deny-all"})," (deny everything until the issue is resolved), or ",(0,r.jsx)(n.code,{children:"quarantine"})," (lock the agent into a deny-all state until an admin clears it)"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["You set ",(0,r.jsx)(n.code,{children:"tamper_behavior"})," in your policy file or in the dashboard."]}),"\n",(0,r.jsx)(n.h2,{id:"built-in-dlp-coverage",children:"Built-in DLP coverage"}),"\n",(0,r.jsxs)(n.p,{children:["DLP scanning runs against every tool call. The hooks ship with ",(0,r.jsx)(n.strong,{children:"65 built-in patterns"})," across four categories. All patterns are active by default."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Category"}),(0,r.jsx)(n.th,{children:"What it catches"}),(0,r.jsx)(n.th,{children:"Examples"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"PII"})}),(0,r.jsx)(n.td,{children:"Personal identifiers across regions"}),(0,r.jsx)(n.td,{children:"US 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)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Financial"})}),(0,r.jsx)(n.td,{children:"Money-related identifiers"}),(0,r.jsx)(n.td,{children:"Credit card numbers (with Luhn validation), IBAN, SWIFT/BIC, US routing numbers, Bitcoin and Ethereum addresses"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Healthcare"})}),(0,r.jsx)(n.td,{children:"HIPAA-relevant identifiers"}),(0,r.jsx)(n.td,{children:"US NPI, US DEA registration numbers"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Secrets"})}),(0,r.jsx)(n.td,{children:"API keys, tokens, connection strings"}),(0,r.jsx)(n.td,{children:"AWS, 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"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["See ",(0,r.jsx)(n.a,{href:"/docs/guides/locale-dlp",children:"Locale-aware DLP"})," for the complete pattern list with regex shapes."]}),"\n",(0,r.jsx)(n.h2,{id:"custom-dlp-rules",children:"Custom DLP rules"}),"\n",(0,r.jsxs)(n.p,{children:["Add organization-specific patterns (internal project codes, proprietary identifiers, internal hostnames) via the dashboard's ",(0,r.jsx)(n.a,{href:"/docs/guides/dlp-rules-editor",children:"DLP Rules Editor"}),", or directly in your policy file:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"dlp_rules:\n - id: internal-project-code\n pattern: 'PROJ-[A-Z]{3}-\\d{6}'\n category: custom\n action: block\n reason: 'Internal project code -- do n
1ot paste into external tools'\n\n - id: customer-account-id\n pattern: 'CUST_[0-9]{10}'\n category: pii\n action: mask\n reason: 'Customer account IDs are masked in audit logs'\n\n - id: internal-jira-ticket\n pattern: '\\bACME-\\d{4,6}\\b'\n category: custom\n action: detect\n reason: 'Track which agents reference internal tickets'\n"})}),"\n",(0,r.jsx)(n.p,{children:"Three actions:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"block"})})," \u2014 the call is denied. The tool never executes."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"mask"})})," \u2014 on Claude Code and Gemini CLI through the Python SDK, replaces the sensitive value in-place and lets the call proceed with the redacted input. The Node hook path never masks. On Codex CLI, Cursor, Kiro, Antigravity, the gateway, and all other surfaces, a mask match becomes DENY."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"detect"})})," \u2014 the call proceeds unchanged. The match is recorded in audit for visibility."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Patterns use Python ",(0,r.jsx)(n.code,{children:"re"})," syntax (PCRE-compatible subset). Test new patterns from the dashboard before publishing -- the editor runs them against sample text live."]}),"\n",(0,r.jsx)(n.h2,{id:"audit-log",children:"Audit Log"}),"\n",(0,r.jsxs)(n.p,{children:["Every governed tool call produces an audit record. The execution status\ndistinguishes ",(0,r.jsx)(n.strong,{children:"did not run"})," from ",(0,r.jsx)(n.strong,{children:"ran and found nothing"}),", so an empty result\ncannot be mistaken for a blocked or missing execution. The local convenience\ncopy is written to ",(0,r.jsx)(n.code,{children:"~/.controlzero/audit.log"})," as JSON lines:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "ts": "2026-04-12T07:30:48.429Z",\n "decision": "deny",\n "tool": "Bash",\n "method": "*",\n "policy_id": "deny-bash",\n "reason": "Shell access requires approval. Use a file tool instead.",\n "args_keys": ["command"],\n "mode": "local",\n "dlp_findings": [],\n "tamper_detected": false,\n "audit_chain_broken": false\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"View recent audit entries:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Last 10 entries\ntail -10 ~/.controlzero/audit.log\n\n# Filter for denials\ngrep '\"deny\"' ~/.controlzero/audit.log\n\n# Filter for DLP blocks\ngrep 'dlp_findings' ~/.controlzero/audit.log | grep -v '\\\\[\\\\]'\n\n# Filter for tamper events\ngrep '\"tamper_detected\": true' ~/.controlzero/audit.log\n\n# Watch live\ntail -f ~/.controlzero/audit.log\n"})}),"\n",(0,r.jsx)(n.p,{children:"The audit log rotates daily with 30-day retention."}),"\n",(0,r.jsx)(n.h2,{id:"enterprise-enrollment",children:"Enterprise Enrollment"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Org-wide enrollment:"})," ",(0,r.jsx)("span",{class:"tier-pill teams",children:"Teams"})," -- Individual hooks work on all tiers. Fleet enrollment, centralized policy management, and audit log aggregation across developers are available in Teams. ",(0,r.jsx)(n.a,{href:"../concepts/pricing",children:"View pricing"})]}),"\n",(0,r.jsx)(n.p,{children:"For fleet-wide governance, enroll machines with the Control Zero backend:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"controlzero install claude-code --api-key cz_live_your_key_here\n"})}),"\n",(0,r.jsx)(n.p,{children:"Once enrolled:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Policies sync"})," from the dashboard to ",(0,r.jsx)(n.code,{children:"~/.controlzero/policy.yaml"})," automatically."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Audit data ships"})," to the backend for centralized visibility."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"DLP rules and detector packs"})," from the dashboard are enforced locally: ",(0,r.jsx)(n.code,{children:"block"})," denies locally. ",(0,r.jsx)(n.code,{children:"mask"})," redacts only on Claude Code and Gemini CLI through the Python SDK; Kiro CLI and every other operational host deny mask matches, as does the Node SDK. Kiro IDE currently evaluates no mask rules because its installed hooks are inert. ",(0,r.jsx)(n.code,{children:"detect"}),"-mode packs and the always-on built-in catalog are observe-only -- they record a finding to the audit trail but do not block."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Fleet status"})," appears on the Devices page in the dashboard."]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"The hook continues to work locally even when the backend is unreachable. Audit entries queue locally and sync when connectivity resumes."}),"\n",(0,r.jsx)(n.h2,{id:"verify-installation",children:"Verify Installation"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'# Claude Code: check hook is registered\ncat ~/.claude/settings.json | python3 -m json.tool | grep -A6 PreToolUse\n\n# Gemini CLI: check hook is registered\ncat ~/.gemini/settings.json | python3 -m json.tool | grep -A8 BeforeTool\n\n# Codex CLI: check hooks.json\ncat ~/.codex/hooks.json | python3 -m json.tool | grep -A8 PreToolUse\n\n# Verify policy signature\ncontrolzero sign-policy --verify-only\n\n# Smoke test the evaluator (should show [Control Zero] Allowed)\necho \'{"tool_name":"Read","tool_input":{"file":"/tmp/test"}}\' \\\n | controlzero hook-check\n\n# Tail the audit log\ntail -f ~/.controlzero/audit.log\n'})}),"\n",(0,r.jsx)(n.h2,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Hook is not firing."})," Confirm ",(0,r.jsx)(n.code,{children:"controlzero"})," is on the PATH for the user running the agent. If you installed in a venv, the agent may not inherit that PATH."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Policy change not taking effect."})," The hook reloads the policy on every tool call. Check that no ",(0,r.jsx)(n.code,{children:"./controlzero.yaml"})," in the current project directory is shadowing the global file. Per-project policies always win over the global one."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Audit log is missing."})," Check permissions on ",(0,r.jsx)(n.code,{children:"~/.controlzero/"}),". The directory must be writable by the user running the agent."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Gemini CLI not blocking denied tools."})," Verify the ",(0,r.jsx)(n.code,{children:"BeforeTool"})," hook uses the nested format (with an inner ",(0,r.jsx)(n.code,{children:"hooks"})," array). The flat format from earlier versions is not recognized by Gemini CLI. Run ",(0,r.jsx)(n.code,{children:"controlzero install gemini-cli --force"})," to update."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Codex CLI not blocking."})," Verify ",(0,r.jsx)(n.code,{children:"~/.codex/hooks.json"})," exists (not the legacy ",(0,r.jsx)(n.code,{children:"config.toml"})," wrapper). Run ",(0,r.jsx)(n.code,{children:"controlzero install c
1odex-cli --force"})," to update. Ensure ",(0,r.jsx)(n.code,{children:"codex_hooks = true"})," is set in ",(0,r.jsx)(n.code,{children:"~/.codex/config.toml"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Tamper detection showing false positives."})," If you edited the policy legitimately, re-sign it: ",(0,r.jsx)(n.code,{children:"controlzero sign-policy"}),". If you did not edit it, investigate. Check file permissions and modification timestamps."]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},28453(e,n,o){o.d(n,{R:()=>i,x:()=>l});var s=o(96540);const r={},t=s.createContext(r);function i(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.