skarn guardhook events

What the real-time guard gates on each AI coding host: the content every hook event scans, how a block is emitted, and which events the shipped configs wire by default.

skarn 0.27.0

How the guard decides

The host pipes a pending action to skarn guard on stdin - a tool call, a submitted prompt, a spawning subagent, a resolved batch of tool outputs - and the guard scans that single action with the full detection engine before it proceeds. It returns one of three verdicts: deny (block the action), ask (escalate to the user's permission prompt), or allow (silent - nothing is emitted at all). Skarn only ever speaks up to tighten, never to auto-approve.

Where a host offers no ask channel, a would-be ask degrades to a block rather than to an allow. Every reason names the matched rule and a redacted preview - never the raw secret. In audit mode (the default, and the only mode an unlicensed guard runs) the guard never decides: it reports the would-be verdict on a non-deciding channel and writes it to the guard log, so you can measure your real would-block rate before enforcing.

This page is generated from the same model that generates the binary's own manual page, and a build gate asserts it against the guard's source and the shipped hook configs - so it describes what the shipped binary does, not what it once did. The equivalent offline reference is man skarn-guard.

Claude Code

Claude Code fires PascalCase events with a JSON envelope on stdin. Five events are wired by default; the two output-scanning events are supported but opt-in because they fire on every tool result.

Routing. Codex shares Claude's PreToolUse and PermissionRequest names, so auto-detection keys on a Codex-only tell (turn_id, or the apply_patch/spawn_agent tools); a Claude event carries none of them and routes to the Claude adapter. VS Code Copilot also shares the envelope, so its own tool-name vocabulary routes it to the Copilot adapter. The shipped Claude PermissionRequest hooks pass --agent claude explicitly anyway.

Fail mode. A non-zero guard exit fails closed on the blocking events. An unparseable event asks in enforce mode.

EventScansEnforceAuditShipped
PreToolUse tool_name plus the flattened tool_input; a Bash command is also carried raw so a tool description cannot poison the package parse hookSpecificOutput.permissionDecision = deny or ask, with the redacted reason in permissionDecisionReason hookSpecificOutput.additionalContext yes
PermissionRequest the same tool_name plus tool_input as PreToolUse, at the permission-decision step hookSpecificOutput.decision.behavior = deny, with the redacted reason on the universal systemMessage systemMessage yes
UserPromptSubmit prompt top-level decision = block, with the redacted reason in reason hookSpecificOutput.additionalContext yes
UserPromptExpansion command_name and expanded_prompt top-level decision = block, with the redacted reason in reason hookSpecificOutput.additionalContext yes
TaskCreated task_title and task_description continue = false, with the redacted reason in stopReason (rolls the task creation back) systemMessage yes
PostToolUse tool_output (falling back to tool_response) of an in-scope tool top-level decision = block, with the redacted reason in reason hookSpecificOutput.additionalContext opt-in
PostToolBatch tool_output and tool_error of every in-scope call in tool_calls[] top-level decision = block, with the redacted reason in reason hookSpecificOutput.additionalContext opt-in
Stop nothing from the event: it triggers a throttled, scoped, offline scan of recent sessions on this machine never blocks (advisory only) systemMessage with suppressOutput yes

"Shipped" means the hook configs in the Skarn repository wire the event by default. An opt-in event is fully supported by the binary - add a block for it to your own config to enable it.

PreToolUse
A deny blocks the call even under bypassPermissions: hooks run before permission-mode checks.
PermissionRequest
A second chokepoint for a call PreToolUse deferred or allowed. The decision has only allow/deny and no reason field, so a would-be ask degrades to deny and additionalContext is invalid here.
UserPromptSubmit
The channel ships content off the machine, so a secret on it is exfiltration. A lone secret is enough to deny here - the destination is not ambiguous. The block is recoverable: the user edits and resubmits.
UserPromptExpansion
The channel ships content off the machine, so a secret on it is exfiltration. The post-expansion text of a slash or custom command - content UserPromptSubmit never sees - gated before it reaches the model.
TaskCreated
The channel ships content off the machine, so a secret on it is exfiltration. A task is subagent fan-out, so it is modeled as the canonical spawn_agent tool. The event has no decision channel and does not support additionalContext.
PostToolUse
Output scanning: catches a secret in a tool RESULT before the model sees it. Opt-in - it fires on every tool result. Prefer PostToolBatch, which sees the same content once per batch.
PostToolBatch
The batch analog of PostToolUse: one invocation per resolved parallel batch, so the cheaper of the two output gates. Opt-in. The batch is scanned as one synthetic interaction named after its first in-scope call.
Stop
The stop advisory. Claude fires Stop at the end of each response, not only at session end; the hourly throttle paces it, so at most once an hour it reports a redacted count and the highest severity of credential exposures found in recent sessions, and points at skarn assess. Never egresses, never prints a secret value.

Cursor

Cursor fires camelCase events, each with its own stdin shape and its own verdict schema - unlike Claude's single envelope. All four blocking events are wired by default.

Routing. The camelCase event names are unique to Cursor, so auto-detection is unambiguous.

Fail mode. Fail-open by default; --strict plus enforce exits 2 (Cursor's schema-agnostic hard block) on an unparseable event.

EventScansEnforceAuditShipped
beforeShellExecution command (mapped to the canonical Bash tool) permission = deny or ask, with the redacted reason in user_message and agent_message none - Cursor's blocking events carry no non-deciding channel, so audit stays silent and relies on the guard log yes
beforeReadFile file_path as the input and content as the result (credential-file recon and dotenv contents) permission = deny (allow/deny only, so a would-be ask degrades to deny) none (see beforeShellExecution) yes
beforeMCPExecution tool_input, under the canonical mcp__<server>__<tool> name permission = deny or ask, with the redacted reason in user_message and agent_message none (see beforeShellExecution) yes
beforeSubmitPrompt prompt continue = false, with the redacted reason in user_message none (see beforeShellExecution) yes

"Shipped" means the hook configs in the Skarn repository wire the event by default. An opt-in event is fully supported by the binary - add a block for it to your own config to enable it.

beforeReadFile
Gated only under --strict, exactly like Claude's Read; the shipped Cursor config passes --strict on this event alone.
beforeSubmitPrompt
The channel ships content off the machine, so a secret on it is exfiltration. No permission channel, so a would-be ask blocks too. The block is recoverable: the user edits and resubmits.

Codex CLI

Codex's hook system mirrors Claude's - PascalCase events and a near-identical PreToolUse JSON - but its verdict semantics differ: permissionDecision ask and a bare allow both FAIL OPEN, so a would-be ask degrades to deny. Codex fires more events than Skarn wires; the rest are evaluated and deliberately left off the matrix (verified against the OpenAI Hooks guide, 2026-07-21): SessionStart and SubagentStart carry only session and subagent metadata (model, permission_mode, agent_id/type), no credential-bearing tool or prompt content; PreCompact and PostCompact carry only a compaction trigger over already-seen transcript the recall scan covers; SubagentStop carries last_assistant_message (so it is NOT content-free), but that post-hoc subagent output lands in the session store the recall scan and the turn-end advisory already cover, so it is a deliberate deferral pending the cross-action daemon the codex README names. Codex has no SessionEnd event in the current reference; if one lands in a later release it is additive.

Routing. Codex shares Claude's event names, so pass --agent codex explicitly. Auto-detection falls back to a Codex-only tell: turn_id (on every turn-scoped event, PermissionRequest included) or the apply_patch/spawn_agent tools.

Fail mode. Fail-open by default; --strict plus enforce emits an explicit deny JSON on an unparseable event.

EventScansEnforceAuditShipped
PreToolUse tool_name plus the flattened tool_input; only Bash carries a raw shell command (apply_patch's command field is patch text) hookSpecificOutput.permissionDecision = deny, with the redacted reason in permissionDecisionReason continue = true plus systemMessage (Codex PreToolUse has no additionalContext) yes
PermissionRequest the same tool_name plus tool_input as PreToolUse hookSpecificOutput.decision.behavior = deny, with the redacted reason in message systemMessage yes
UserPromptSubmit prompt top-level decision = block, with the redacted reason in reason systemMessage yes
PostToolUse tool_response of an in-scope tool top-level decision = block, with the redacted reason in reason systemMessage opt-in
Stop nothing from the event: it triggers a throttled, scoped, offline scan of this machine's recent codex sessions never blocks (advisory only) systemMessage with suppressOutput; it carries no decision, so Codex never continues the turn yes

"Shipped" means the hook configs in the Skarn repository wire the event by default. An opt-in event is fully supported by the binary - add a block for it to your own config to enable it.

PermissionRequest
The approval-flow gate: any deny wins, closing PreToolUse's updatedInput and ask fail-open edges.
UserPromptSubmit
The channel ships content off the machine, so a secret on it is exfiltration.
PostToolUse
Output scanning. Opt-in - it fires on every tool result.
Stop
The turn-end advisory - Codex Stop is turn-scoped (it fires when the agent finishes a turn), not a session-end signal. Handled at run() level by isSessionEndEvent (guard.zig), which scans with --agent codex; at most once an hour it reports a redacted count and the highest severity of credential exposures in recent codex sessions and points at skarn assess. Claude and Codex share one hourly advisory budget (the guard-assess-advisory throttle), so activity on either host can suppress the other's nudge for that hour - a deliberate global budget, not a per-host one. Never egresses, never prints a secret value.

GitHub Copilot CLI

The Copilot CLI's native payload is camelCase (toolName / toolArgs / sessionId) and carries no event-name field, so the event is inferred from the field shape - except permissionRequest, whose payload is a subset of preToolUse's and which the config entry therefore names with --event permissionRequest. preToolUse and permissionRequest both block; the other events run full detection and write the audit log but cannot decide. GitHub documents fourteen Copilot CLI hook events; Skarn declares five of them (preToolUse, permissionRequest, userPromptSubmitted, userPromptTransformed, postToolUse) plus preMcpToolCall, and evaluates the rest as deliberately not wired (verified against the GitHub hooks reference, 2026-07-30): sessionStart carries an initialPrompt (scannable), but a session-open detection arm beyond userPromptTransformed is out of this round's scope; subagentStart and subagentStop carry subagent metadata (agentName, transcriptPath), and the subagent's own tool calls are gated by preToolUse while any post-hoc output lands in the session store the recall scan covers; postToolUseFailure carries the failed call's toolArgs plus its error text, which can hold output preToolUse never saw, so leaving it unwired is a deliberate coverage deferral of the same class as the opt-in postToolUse output scan; errorOccurred and notification carry human-readable error and message text (a low-value surface deferred this round, not a claim they can never carry a secret); preCompact carries a compaction trigger and any customInstructions over already-seen transcript; agentStop (Stop) and sessionEnd carry only a stopReason/reason over transcript the recall scan covers. Every one of these is a deliberate coverage deferral, not an assertion that the event can never carry a credential.

Routing. The camelCase field set is the CLI tell. A Copilot payload in Claude-compat mode is byte-indistinguishable from a real Claude event (field names AND tool names), so the shipped Copilot configs always pass --agent copilot explicitly. The CLI loads hooks from policy, then the repository, then the user, then inline repository settings, then inline user settings, then plugins, and how several hooks for one event compose differs by event. On preToolUse any hook returning deny blocks the tool, so a Skarn deny there is final; measured on CLI 1.0.70 the entries after the denying one did not run at all, and on 1.0.61 under SDK 1.0.1 the host's own SDK-registered callback was never invoked for that call either. On permissionRequest every configured hook runs and their outputs are MERGED with later outputs overriding earlier ones, so a Skarn deny there is not final: a hook loading after it that answers allow replaces the decision. That is why preToolUse remains the durable gate and permissionRequest is a second chokepoint rather than a stronger one. A top-level disableAllHooks: true in the repository's .github/copilot/settings.json skips the user- and repository-delivered hooks for that repository, the operator's own included, and logs nothing when it does; whether machine policy hooks survive it is unverified. It silences the layers it reaches across events (sessionStart as well as preToolUse) and leaves the host application's own SDK-registered callbacks running, so doctor reads that file and reports the file layer as inert rather than counting it as protection or calling the machine unprotected.

Fail mode. preToolUse fails CLOSED on a non-zero exit, but a hook TIMEOUT fails OPEN (the shipped configs set timeoutSec 10). permissionRequest treats exit 2 as a deny.

EventScansEnforceAuditShipped
preToolUse toolName plus toolArgs (an object, or a JSON string that is parsed and flattened) top-level permissionDecision = deny or ask, with the redacted reason in permissionDecisionReason additionalContext yes
permissionRequest toolName, plus toolArgs when the payload carries it top-level behavior = deny, with the redacted reason in message; that decision short-circuits the normal permission flow, but not other hooks - every configured permissionRequest hook runs and later outputs override earlier ones, so a hook loading after Skarn can answer allow and replace the deny none yes
userPromptSubmitted prompt nothing is emitted - the CLI ignores this event's hook output none yes
userPromptTransformed transformedPrompt (the transformed, model-facing prompt text; the payload also carries the pre-transformation prompt, and this row is inferred on transformedPrompt before the prompt-only userPromptSubmitted branch) nothing is emitted - this event's only output channel is modifiedTransformedPrompt, which rewrites the model-facing prompt, and Skarn declines that mutation channel by policy; there is no block or decision channel on this event, so it is detection and audit-log ONLY none opt-in
preMcpToolCall arguments, under the canonical mcp__<server>__<tool> name nothing is emitted - this event's output controls only MCP request metadata none opt-in
postToolUse toolResult advisory only - never blocks additionalContext opt-in

"Shipped" means the hook configs in the Skarn repository wire the event by default. An opt-in event is fully supported by the binary - add a block for it to your own config to enable it.

preToolUse
The durable gate on this host: a deny from ANY hook blocks the tool, so Skarn's answer here cannot be replaced by a later one. It is also the only Copilot event where ask is a real interactive prompt rather than a degraded deny.
permissionRequest
The second chokepoint on this host: the permission-decision step for a call preToolUse deferred or allowed. GitHub documents its payload as a subset of preToolUse's and no field separates the two, so the guard never infers it - the shipped config entries pass --event permissionRequest, and an unpinned payload is read as preToolUse. A would-be ask degrades to deny (the schema has allow and deny and no ask value). Its deny is not final the way a preToolUse deny is: outputs from several permissionRequest hooks merge last-writer-wins, and the load order is policy, repository, user, inline settings, plugins - so a policy-delivered Skarn deny is the MOST overridable placement and a plugin-delivered one the least. Audit is silent by design: the only other output fields are message and an interrupt that stops the whole agent, and a message with no behavior would ride into another hook's merged decision, so there is no channel here that advises without deciding.
userPromptSubmitted
The channel ships content off the machine, so a secret on it is exfiltration. A secret typed into a Copilot CLI prompt is detected and logged but NOT blocked before it reaches the provider - unlike Claude Code and Codex, whose prompt-submit hooks can block. A host limitation, not a Skarn one.
userPromptTransformed
The channel ships content off the machine, so a secret on it is exfiltration. On this event Skarn detects and writes the audit log only; it never blocks or decides, because the event's sole output channel is modifiedTransformedPrompt (a prompt rewrite) which Skarn declines by policy - it observes and blocks but never rewrites model-facing content, a Skarn policy rather than a host limitation. Opt-in, not wired in the shipped configs: a config update reaches users independently of the binary, and a binary predating this event would scan the payload as userPromptSubmitted (it carries prompt), so the opt-in row avoids that ambiguity until a release carrying the arm is current.
preMcpToolCall
Supported but not wired in the shipped configs: it cannot block or advise, so it buys detection coverage in the guard log only. Added in Copilot CLI 1.0.51 and still firing (no removal through 1.0.73), and still absent from GitHub's hooks-reference event table on re-verification 2026-07-30, which is why it gains no decision channel here: the one output the SDK documents for it is metaToUse, MCP request metadata, and Skarn does not emit a verdict schema the vendor does not publish. The row is kept version-scoped because older supported CLIs still fire it.
postToolUse
Output scanning. Opt-in - it fires on every tool result.

GitHub Copilot (VS Code agent mode)

VS Code agent mode speaks a second Copilot dialect: PascalCase event names in a Claude-shaped envelope, camelCase tool_input keys, and VS Code's own tool-name vocabulary (run_in_terminal, create_file, ...), which Skarn maps to canonical names. MCP calls arrive as PreToolUse with an mcp_<server>_<tool> name.

Routing. VS Code discovers hooks from ~/.claude/settings.json (chat.hookFilesLocations), so a machine already running Skarn's Claude guard receives VS Code Copilot events. The VS Code tool-name vocabulary routes them to the Copilot adapter instead of a silent Claude fast-allow; the shipped configs pin --agent copilot.

Fail mode. VS Code blocks only on exit 2; any other non-zero exit fails OPEN.

EventScansEnforceAuditShipped
PreToolUse tool_name (VS Code vocabulary, mapped to canonical) plus the flattened tool_input hookSpecificOutput.permissionDecision = deny or ask - both fully honored (the exact inverse of the CLI, where the decision is top-level) systemMessage yes
UserPromptSubmit prompt top-level decision = block, with the redacted reason in reason systemMessage yes
PostToolUse tool_response top-level decision = block, with the redacted reason in reason systemMessage opt-in

"Shipped" means the hook configs in the Skarn repository wire the event by default. An opt-in event is fully supported by the binary - add a block for it to your own config to enable it.

UserPromptSubmit
The channel ships content off the machine, so a secret on it is exfiltration. Unlike the Copilot CLI, the VS Code host honors a prompt-submit block. On the cloud agent an ask degrades to deny (non-interactive).
PostToolUse
Output scanning. Opt-in - it fires on every tool result.

Gemini CLI

Gemini CLI fires eleven PascalCase events with a JSON envelope on stdin; hooks ship on by default since v0.26.0. Skarn gates three of them - BeforeTool (the enforcing pre-execution gate), BeforeAgent (the submitted prompt, an egress channel), and AfterTool (the tool result, opt-in) - and every other event fast-allows. The stdout protocol is strict: the host requires that a hook print nothing to stdout but its final JSON object, so the guard's emit arm is the sole stdout writer and an allow prints nothing at all. Gemini's PUBLISHED output schema documents only allow and deny (alias block), with no ask value; gemini-cli 0.49.0 does route an undocumented decision "ask" on BeforeTool to an interactive confirmation, but skarn deliberately does not emit it: a version that drops the undocumented behavior would read "ask" as no decision and let the call through, so a would-be ask degrades to deny (fail-closed on an undocumented channel). Timeouts are MILLISECONDS here, not seconds (the shipped entries use 5000).

Routing. The Before*/After* event vocabulary is unique to Gemini - no other host uses those names - so auto-detection is unambiguous; the shipped configs pin --agent gemini anyway, the same policy every other host follows. MCP tools arrive named mcp_<server>_<tool> with SINGLE underscores, so the guard keys MCP treatment on that prefix rather than on the canonical mcp__ form.

Fail mode. A non-2 non-zero exit is a warning and fails open; exit 2 blocks with stderr as the reason. --strict plus a licensed enforce exits 2 on an event the guard cannot parse (malformed or oversized) and on a scan that cannot run (unreadable rules); an out-of-scope action still fast-allows, and an unlicensed guard is forced to audit and exits 0. A hook that runs past its timeout has its decision discarded and the call proceeds, and the observed wait tracks the hook's own runtime up to about 30 seconds rather than the configured value, so the timeout bounds nothing - a fast guard is the real protection. From gemini-cli 0.56.0 a crashed, timed-out, or exit-2 hook also prints a 'Hook execution for <event>: N succeeded, M failed' summary and a '[WARNING] Hook(s) [skarn-guard] failed' line on stderr; only the exit-2 case blocks.

EventScansEnforceAuditShipped
BeforeTool tool_name (mapped from Gemini's own vocabulary: run_shell_command, write_file, replace, read_file, read_many_files, web_fetch, google_web_search, invoke_agent) plus the flattened tool_input; a run_shell_command command is also carried raw so a tool description cannot poison the package parse top-level decision = deny, with the redacted reason in reason (a would-be ask degrades to deny: the published schema documents no ask value, and the undocumented one gemini-cli 0.49.0 honors here would fail open on any version that drops it) systemMessage yes
BeforeAgent prompt top-level decision = deny, with the redacted reason in reason systemMessage yes
AfterTool tool_response, an object carrying llmContent, returnDisplay, and an optional error, flattened into the scanned bytes; from gemini-cli 0.56.0 llmContent arrives wrapped in <untrusted_context> tags, which are scanned through and trigger nothing on their own top-level decision = deny, with the reason replacing the tool result before the model sees it systemMessage opt-in

"Shipped" means the hook configs in the Skarn repository wire the event by default. An opt-in event is fully supported by the binary - add a block for it to your own config to enable it.

BeforeTool
The entry timeout is in milliseconds (5000), unlike Claude's seconds. Gemini reads ONE command field on every platform - there is no windows variant - so setup renders the form the machine it configures needs, and a project-scope file committed from POSIX fails open (a warning) on a native-Windows teammate. MCP calls arrive as mcp_<server>_<tool>; skarn always rewrites the name to the canonical mcp__ form so every tool-keyed detector sees an MCP call, recovering the real server/tool split from mcp_context when it is present, and never fast-allows either way. The event also offers hookSpecificOutput.tool_input, which rewrites the model's arguments - skarn surfaces and blocks but never rewrites, so that channel is deliberately unused.
BeforeAgent
The channel ships content off the machine, so a secret on it is exfiltration. A lone secret is enough to deny here - the destination is not ambiguous. A deny blocks the turn before any request leaves for the model provider and discards the message from history (gemini-cli 0.56.0 renders it as 'Agent execution blocked: <reason>' on stderr and exits 0); it is recoverable, the user edits and resubmits.
AfterTool
Output scanning. Opt-in - it fires on every tool result.

Grok Build

Grok Build fires a PascalCase event set with a JSON envelope on stdin, in a camelCase dialect of its own: hookEventName carries a snake_case VALUE ("pre_tool_use"), the tool fields are toolName/toolInput/toolUseId/toolInputTruncated, and PostToolUse adds toolResult. Skarn gates three of those events - PreToolUse (the only enforcement point grok offers), UserPromptSubmit (the submitted prompt, an egress channel), and PostToolUse (the tool result, opt-in) - and every other event fast-allows. Grok has NO non-deciding channel: its passive events ignore stdout entirely and every recognized PreToolUse output decides, so the guard emits nothing at all in audit mode and the would-be verdict lands only in the guard log, which skarn guard report reads. Timeouts are seconds (the shipped entries use grok's 5-second default). The honest posture is fail-open: every grok hook failure, timeout, crash or malformed output is recorded for the session scrollback and blocks nothing, so an explicit deny is the only thing that stops a call.

Routing. The camelCase hookEventName key is unique to grok - Claude, Codex, Cursor and VS Code Copilot spell it hook_event_name, and the Copilot CLI carries no event-name field - so an unpinned grok payload auto-detects on key presence, before the Copilot CLI's toolName/sessionId structural tells that it would otherwise trip. Grok is also the ONE place the guard overrides an explicit --agent pin: grok natively runs the hooks configured in ~/.claude/settings.json and ~/.cursor/hooks.json and invokes them with its own dialect, so a hook pinned --agent claude is handed grok events it cannot read. When the process environment carries GROK_HOOK_EVENT (grok's runner injects it for every hook and strips any value a config sets) AND the payload carries hookEventName, the event routes to the grok adapter whatever the pin says. Neither signal alone moves a pin, and a real Claude payload under a claude pin is unaffected either way. MCP tools arrive under their resolved server__tool name with no prefix; the guard rewrites that to the canonical mcp__ form so every tool-keyed detector sees an MCP call, and an MCP call never fast-allows.

Fail mode. Every failure fails open, which is grok's own posture for hooks: a non-zero exit other than 2 is recorded and blocks nothing. Exit 2 is an explicit PreToolUse deny, and a deny on stdout is honored regardless of the exit code (a stdout allow paired with exit 2 still denies, so the guard never pairs the two). --strict plus enforce turns an unparseable event into one stderr line and exit 2.

EventScansEnforceAuditShipped
PreToolUse toolName (mapped from grok's own vocabulary: run_terminal_command, search_replace, read_file, web_fetch, web_search, spawn_subagent) plus the flattened toolInput; a run_terminal_command command is also carried raw so a tool description cannot poison the package parse top-level decision = deny, with the redacted reason in reason (a would-be ask degrades to deny: grok's schema has no ask value) nothing - grok has no non-deciding channel, so the would-be verdict goes to the guard log only yes
UserPromptSubmit prompt nothing - grok ignores stdout on this event, so it is detection and audit only nothing - the would-be verdict goes to the guard log only yes
PostToolUse toolResult, typed as arbitrary JSON, so a string result and an object one are both flattened into the scanned bytes nothing - grok ignores stdout on this event nothing - the would-be verdict goes to the guard log only opt-in
pre_tool_use the same event as PreToolUse top-level decision = deny nothing opt-in
user_prompt_submit the same event as UserPromptSubmit nothing nothing opt-in
post_tool_use the same event as PostToolUse nothing nothing opt-in

"Shipped" means the hook configs in the Skarn repository wire the event by default. An opt-in event is fully supported by the binary - add a block for it to your own config to enable it.

PreToolUse
The shipped entry carries NO matcher: an omitted matcher matches every tool and the guard scopes in-process, which also keeps the file free of a matcher list that would drift from the adapter. grep and list_dir are deliberately unmapped, so they pass through and fall out of scope. toolInputTruncated is tolerated: the guard scans the bytes grok hands it and claims nothing about what was cut.
UserPromptSubmit
The channel ships content off the machine, so a secret on it is exfiltration. A lone secret is enough to flag here - the destination is not ambiguous - but grok gives the hook no way to stop the turn, so the record is the whole product. A matcher on this event is ignored with a warning by grok, which is the second reason the shipped file carries none.
PostToolUse
Output scanning. Opt-in - it fires on every tool result.
pre_tool_use
The wire form of PreToolUse: grok's stdin envelope spells the event value in snake_case while its config files use the PascalCase name, and the adapter recognizes both.
user_prompt_submit
The channel ships content off the machine, so a secret on it is exfiltration. The wire form of UserPromptSubmit.
post_tool_use
The wire form of PostToolUse.

Antigravity CLI (agy)

The Antigravity CLI (agy) fires five PascalCase events - PreToolUse, PostToolUse, PreInvocation, PostInvocation, Stop - with a camelCase protojson envelope on stdin and NO event-name field: the hooks.json entry that fired is the only thing that names the event, so the adapter reads the event off the payload shape (a toolCall with no error key is the PreToolUse it gates; every other shape fast-allows). PreToolUse is the whole guard surface on this host: PreInvocation carries no prompt text and PostToolUse carries no tool output, so there is no prompt-submit gate and no result scan (verified on agy 1.1.18 and 1.1.19). The stdout contract is a flat {"decision","reason"} object, and agy FAILS CLOSED on everything but an explicit allow: a deny blocks, and a crash, a timeout (seconds; the shipped entries use 5), invalid JSON, and an empty {} (a missing decision) all block the call and the model sees the hook-failure text; only an empty stdout was observed to proceed (1.1.19), which is undocumented and which the guard never relies on. The guard therefore answers EVERY tool event with an explicit decision: enforce prints deny or allow, audit prints allow with the would-be verdict in reason (agy has no non-deciding channel), an out-of-scope tool gets an explicit allow, and a non-tool event gets the empty object. ask is not a block on agy (it hands the call to the permission layer, which auto-denies in print mode), so a would-be ask degrades to deny. An allow never grants a permission: agy's own permission check still runs after the hook.

Routing. The camelCase toolCall / conversationId / workspacePaths / artifactDirectoryPath field set is unique to agy - Claude, Codex, Cursor, Gemini, and VS Code Copilot use snake_case envelopes, the Copilot CLI spells its fields toolName/toolArgs/sessionId, and grok's tell is hookEventName - so an unpinned agy payload auto-detects (before the Claude PascalCase fallback, so a future payload that adds hook_event_name still routes here); the shipped configs pin --agent antigravity anyway, uniform policy. The agy tool vocabulary on the hook wire was pinned live (1.1.19): a file write fires as write_to_file (TargetFile/CodeContent) and an edit as replace_file_content or multi_replace_file_content (TargetFile/ReplacementContent), while run_command, view_file, browser_*, the read_url_content/open_browser_url/search_web network steps, and the agency_tool_call/invoke_subagent/browser_subagent subagent steps keep their step-type spellings; the step-type spellings of the write tools (file_change, code_action, edit_notebook) stay mapped and matched as drift aliases. The MCP dispatch is call_mcp_tool on the hook wire (the binary's hook tool table) and mcp_tool in the transcript the scan parser reads; both are rewritten to the canonical mcp__ form (from the ServerName/ToolName args), so an MCP call never fast-allows. The grok override still applies: a hook process grok spawned routes to grok whatever the pin.

Fail mode. Fail-closed by the host's design, on every outcome but an explicit allow: a crashed, slow (past the entry's timeout, in seconds), or malformed skarn BLOCKS the tool call, and a missing skarn binary blocks every matched call the same way (sh -c exits 127). Enforce and audit are both hard gates here - audit cannot fail more open than enforce, so a machine where the hook is wired and the binary is gone blocks matched calls until the binary is back or the entry is removed. --strict plus a licensed enforce answers an event the guard cannot parse with an explicit deny; without --strict it answers an explicit allow, which is the only way to fail open on this host.

EventScansEnforceAuditShipped
PreToolUse toolCall.name mapped to canonical (run_command to Bash; write_to_file, file_change, and code_action to Write; replace_file_content and multi_replace_file_content to Edit; notebook_edit and edit_notebook to NotebookEdit; view_file to Read; browser_subagent, invoke_subagent, and agency_tool_call to spawn_agent; every other browser_* plus execute_browser_javascript, capture_browser_screenshot, read_url_content, and open_browser_url to WebFetch; search_web to WebSearch; call_mcp_tool and mcp_tool to mcp__) plus the flattened toolCall.args, with the TargetFile/AbsolutePath path lifted to a canonical file_path line for the write-target detectors; a run_command's CommandLine is also carried raw so toolSummary and toolAction cannot poison the package parse top-level decision = deny, with the redacted reason in reason (a would-be ask degrades to deny: ask is not a block on agy); an allow is printed explicitly, because the contract requires a decision and an empty {} is a deny top-level decision = allow, with the would-be verdict (and the unlicensed advisory) in reason - agy has no non-deciding channel yes

"Shipped" means the hook configs in the Skarn repository wire the event by default. An opt-in event is fully supported by the binary - add a block for it to your own config to enable it.

PreToolUse
The shipped matcher names the scoped tools (run_command|write_to_file|replace_file_content|multi_replace_file_content|file_change|code_action|notebook_edit|edit_notebook|browser_.*|execute_browser_javascript|capture_browser_screenshot|read_url_content|open_browser_url|search_web|call_mcp_tool|mcp_tool|agency_tool_call|invoke_subagent) rather than * so a fail-closed host does not pay a hook round-trip, and its failure modes, on read-only steps. view_file is deliberately NOT in the shipped matcher: without --strict the adapter fast-allows every read, so wiring it would put agy's fail-closed exposure on every file view for no scan; a --strict install adds view_file| to the matcher, which then path-gates the named credential stores (content-keyed read detectors need tool output agy never provides, so a dotenv read is not detected on this host). The event name is not in the payload: a PreToolUse is recognized by its toolCall with no error key, and a PostToolUse (toolCall plus error) is answered with the empty object it expects and scanned for nothing, since it carries no tool output. The hook runs before agy's permission check, and skarn's allow grants nothing.

Wiring it up

skarn setup detects the AI coding agents installed on the machine and merges the guard hook into each one's native config in audit mode - existing hooks untouched, every modified file backed up and rewritten atomically, and the run ends with a self-test that proves the wiring end to end. skarn doctor then verifies it stays alive: it names any event your installed config does not gate, and it catches a dead hook (a wired command whose binary no longer exists), which every host silently fails open on.

Full option reference: the guard, setup, and doctor sections of the manual.