grok-hooks-valid¶
.grok/hooks/*.json must use Grok's hook events, handler types and fields
| Severity | error (auto) |
| Autofix | - |
| Since | v0.20.0 |
| Repo Types | grok-project |
| Category | Grok Build |
Why¶
.grok/hooks/*.json automates shell commands and HTTP calls during Grok
Build agent lifecycle events — right before a tool runs, when a session
starts, when the agent finishes. The files are committed, so those commands
are shared by everyone working on the project.
Grok prints no diagnostic when it refuses something. A rejected file, a
dropped matcher group, a skipped event and a discarded handler all look
identical from the outside: a hook that had nothing to do. grok inspect
--json reports no configuration warning for any of them. This rule reads
each file the way Grok's loader does, so the mistake shows up before the
silence does.
Grok reads the directory as a flat *.json glob and merges every file in
it, so a finding names the file it belongs to. A file in a subdirectory, or
under any other extension, is never loaded at all.
The commands themselves are a separate concern — they are scanned for risky
patterns by hooks-dangerous and can be inventoried
against an explicit allowlist with
hooks-prohibited.
Severity¶
A finding's severity is how much of the file the defect costs.
Errors — nothing in the file runs. Grok reads it with a parser that refuses the whole document rather than the offending part, so one mistake here costs every hook in the file, including the ones under other events.
- Invalid JSON, or a non-finite number (
NaN,Infinity,-Infinity), which Grok's parser does not accept. - A UTF-8 byte-order mark at the start of the file. Grok's reader does not strip one and refuses the whole file; most editors and every JSON viewer hide the mark, so the file looks correct everywhere you would go to check it. Only Grok is reported for this — no other host skillsaw supports has been measured, and reporting one that tolerates a BOM would be a false positive.
- No top-level
hooksobject, or one that is not an object. - An event whose value is not an array, a matcher group that is not an
object, a group with no
hookskey or a non-array one, or a handler that is not an object. - A handler with no
type, or anullone. There is no default, and Grok reads anullas the key being absent. - A
matcherthat is not a string — a list or an object, say. Grok's field is a string; anything else never reaches the regex compiler. - A known field carrying the wrong JSON type:
type,commandandurlare strings,timeoutis a non-negative integer, andenvis an object whose values are strings."timeout": "30",30.0,-1andtrueeach cost the file. A largetimeoutis fine —StopandSubagentStopdefault to 600 seconds because gates run test suites — up to18446744073709551615. Grok reads the field as a 64-bit unsigned integer and JSON has no integer width, so one digit past that refuses the file exactly as30.0does. A JSONnullis not one of those wrong types: it is the key being absent, and costs whatever omitting the key costs — nothing fortimeout,envor a field the handler's type does not require, and one handler for acommandhandler'scommandor anhttphandler'surl.
Warnings — the file loads and something in it does not fire.
- An unrecognized event name. Grok skips the entries under it so the rest of the file still loads, which is why a typo is invisible at runtime.
- A
matcherthat does not compile. Grok compiles matchers with Rust's regex engine, which differs from Python's in both directions, so skillsaw checks both and warns rather than errors. Unicode classes, the character-class set operators, the(?<name>...)capture group and the\zanchor are Rust's spelling: skillsaw rewrites them rather than calling a working matcher broken. Look-around ((?=,(?!,(?<=,(?<!), backreferences (\1,(?P=name)), the\Zanchor, and conditional, comment and atomic groups ((?(1),(?#,(?>) are the other direction — Python compiles them and Rust does not, so skillsaw names the construct instead of waiting for a compile error that never comes. The rest is the syntax the two dialects share. A matcher longer than 1,000 characters is left alone: Grok sets no length limit, so length is not a defect, and a hooks file is untrusted input that the syntax check has no reason to scan without a bound. - A
commandhandler with nocommandor anullone, anhttphandler with nourlor anullone, or atypeother thancommandandhttp. Each drops that one handler; siblings in the same group still run. - An empty
hooksobject or event array — valid, and it configures nothing.
Info — the file loads, the hook runs, and one thing in it is ignored.
- An
enventry naming a variable the hook runner injects (GROK_HOOK_EVENT,GROK_HOOK_NAME,GROK_SESSION_ID,GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR). The runner's value always wins, so the declared one never reaches the process. - A
matcheronStoporUserPromptSubmit. Those events always fire, so the pattern is kept in the configuration and never consulted — Grok does not even compile it.
Event names¶
Grok accepts several spellings of each event and normalizes them, so a hooks file shared with Claude Code or Cursor loads unchanged. All of these are accepted and none is a finding:
- The 15 names Grok documents:
SessionStart,SessionEnd,UserPromptSubmit,PreToolUse,PostToolUse,PostToolUseFailure,PermissionDenied,Stop,StopFailure,StopCancelled,Notification,SubagentStart,SubagentStop,PreCompact,PostCompact. SubagentEnd, Grok's documented alias forSubagentStop.- The
snake_casespelling of each, the wire name the hook itself receives inGROK_HOOK_EVENT. - The
camelCasespelling of each, with one exception:userPromptSubmitis not accepted. WriteUserPromptSubmit,user_prompt_submit, or Cursor'sbeforeSubmitPrompt. - Cursor's per-operation names, which map to the generic tool events:
beforeShellExecution,beforeMCPExecutionandbeforeReadFilebecomePreToolUse;afterShellExecution,afterMCPExecution,afterFileEdit,afterAgentResponseandafterAgentThoughtbecomePostToolUse.
Examples¶
Bad — the string timeout costs every hook in the file, including the
Stop hook under another event:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "./scripts/version.sh", "timeout": "10" }
]
}
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "make lint" }] }
]
}
}
Good — one matcher group per event, each handler carrying the field its type needs:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|run_terminal_command",
"hooks": [
{ "type": "command", "command": "./scripts/audit-command.sh", "timeout": 5 }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "make lint", "timeout": 600 },
{ "type": "http", "url": "https://hooks.example.com/turn-ended", "timeout": 10 }
]
}
]
}
}
How to fix¶
- Write
timeoutas a non-negative integer of seconds (30, not30.0or"30"), and give aStoporSubagentStopgate enough of them for the command it runs. - Give every handler a
type, and the field that type needs:commandfor acommandhandler,urlfor anhttpone. - Keep
envvalues as strings, and set anything the runner already injects inside the script rather than inenv. - Match one of the event spellings above, and drop a
matcherfromStopandUserPromptSubmit— put the condition in the script, which receives the event as JSON on stdin. - Split a matcher into a group per pattern rather than reaching for a construct Rust's regex engine does not have.
Grok adds events faster than skillsaw releases. Rather than turning the rule off, name the new one:
rules:
grok-hooks-valid:
# An event name Grok dispatches that this release has not heard of.
extra-events:
- PreSomethingNew
Configuration¶
| Parameter | Description | Default |
|---|---|---|
extra-events |
Additional hook event names to accept, for events newer than this skillsaw release | [] |
Run skillsaw explain grok-hooks-valid to see this documentation and the rule's effective configuration in your terminal.