# antigravity-hooks-valid hooks.json must use Antigravity's hook events, handler types and fields | | | |---|---| | **Severity** | error (auto) | | **Autofix** | - | | **Since** | v0.20.0 | | **Repo Types** | antigravity, antigravity-plugin | | **Category** | [Google Antigravity](antigravity.md) | ## Why A customization root — `.agents/`, `.agent/`, `_agents/` or `_agent/` — can carry a `hooks.json` that runs shell commands and injects prompts around Antigravity's lifecycle events: before and after a tool runs, before and after an invocation, when a session starts, and when the agent stops. The file is committed, so the hooks are the team's, not one developer's. A plugin under `plugins//` carries its own `hooks.json` alongside its manifest. Antigravity tells nobody when it refuses one. Measured against `agy` 1.1.26: every load-time rejection drops the **whole file**, logs a single `failed to parse hooks.json at ` line to the debug log, and exits 0 — so a sibling hook that was working stops running and CI stays green. A key the parser does not recognise is quieter still: it is discarded, the file loads, and the hook it configures simply never fires. This rule reports both, and says which is which. Command execution is also scanned for security by [`hooks-dangerous`](hooks-dangerous.md) and can be inventoried against an allowlist with [`hooks-prohibited`](hooks-prohibited.md). ## Severity **Errors** — Antigravity loads no hook from the file: - Invalid JSON, a non-finite number (`NaN`, `Infinity`, `-Infinity`), a trailing comma or a comment. The parser is strict JSON. - A UTF-8 byte-order mark (BOM). Remove it; the loader does not strip it. - A non-null root that is not an object of named hooks. - A named hook that is not an object. An `enabled` key at the **top level** is reported this way too, with its own wording: every top-level key is a hook *name*, so there is no file-level switch to write there. - An `enabled` inside a named hook that is not a boolean. - An event whose value is not an array; a group or a handler that is not an object; a `matcher` that is not a string; a group's `hooks` that is not an array. - A handler `type` outside `command` and `prompt`. The comparison is case-sensitive: `"COMMAND"` is refused. - A command hook carrying `prompt` or `model`, or a prompt hook carrying `command`. - A `timeout` that is not a whole number, or one outside `-2147483648` to `2147483647`. It is a 32-bit integer: `0` and negative values load; a float, a string, or a number past either end does not. **Warnings** — ignored settings, empty commands, or a file intended for another host: - A file written in **another host's nested shape**: events under a top-level `hooks` object beside non-object metadata such as `version`, `description` or `$schema`. Without metadata, this finding requires all nested keys to name known events and at least one flat event to contain a group with no handler of its own. Other shapes receive the ordinary event and handler diagnostics. `.agents/` is a directory name four ecosystems share, so a Claude, Codex or Cursor hooks file lands here often. One finding replaces the pile the ordinary walk would produce. Its severity is **fixed at warning** — a configured `severity:` does not reach it — because the shape says the file targets another tool, and an ERROR would fail CI for a repository that never configured Antigravity. Null siblings do not count as metadata. An object-valued sibling or a top-level `enabled` also keeps the ordinary named-hook validation. A `hooks` object holding only `PreToolUse` with no metadata sibling is excluded: the two hosts' group shapes coincide there and the file really does dispatch. - An event name Antigravity does not dispatch. Known names match case-insensitively, so `pretooluse` is fine; `SessionEnd` is not an event and its hooks never fire. - An unknown key on a handler or on a group. `env`, `cwd`, `name` and `background` are all discarded, so a hook written with one does not do what its author expects. - A command hook with no `command`, or an empty one. It loads and runs nothing. While an error stands, the warnings are held back: nothing in the file has loaded, so nothing has been ignored yet. Struct field names match without regard to case: `Enabled`, `Matcher`, `Hooks`, `Command`, `Type`, `Prompt`, `Model` and `Timeout` use the same contracts as their canonical spellings. Hook names remain case-sensitive, and handler type *values* still accept only lowercase `command` and `prompt`. Command scans and generated documentation read the same decoded fields. ## What is not reported - **The `matcher` pattern.** Antigravity never compiles it at load time — an unclosed character class loads clean — so no linter can say whether a given pattern will be accepted, and the regex engine is unverified. `""` and `"*"` are the documented catch-alls. - **A hook-level `"enabled": false`.** It is the documented per-hook switch and a valid thing to commit. The security rules still read the commands under it, because the command ships in the repository either way. - **A named hook called `enabled`.** With an object value it is an ordinary hook and loads, so it is checked like any other rather than reported. Only a non-object value there kills the file. - **A `prompt` hook with no `prompt` text**, and **an empty group or event array**. All load. - **Valid repeated keys.** Repeated hook names and events replace their earlier values, including differently capitalized event keys. Handler string fields apply in encounter order, but a later null retains their previous string value. A null event or `hooks` list clears that array. An earlier invalid type still rejects the file, even when the containing event or named hook is replaced. Handler type and command/prompt conflicts are checked after that handler's fields have been decoded, so replacing an unsupported type string with a supported one is accepted. - **A null root.** It is an explicit empty configuration, like `{}`. - **Finite numbers outside Python float range in ignored fields.** `metadata: 1e400` is valid JSON and receives only the ignored-key warning. The same value in `timeout` still fails its integer type check. - **Null fields and empty strings are not type errors for string fields.** Null events and entries, an empty hook name, and empty `type`, `prompt`, `model` or `matcher` values are accepted. A missing, null or empty `command` still earns the no-command warning above. An empty string in `timeout` or `enabled` is a type error and rejects the file. ## Event names Two shapes, and the event decides which: - `PreToolUse`, `PostToolUse` — an array of `{matcher, hooks: [handler, …]}` groups. - `PreInvocation`, `PostInvocation`, `Stop`, `SessionStart` — a flat array of handlers. A `matcher` written on one of these is ignored. ## Examples **Bad** — a fractional `timeout`, which costs every hook in the file: ```json { "shell-audit": { "PreToolUse": [ { "matcher": "run_command", "hooks": [ { "type": "command", "command": "./scripts/audit-command.sh", "timeout": 1.5 } ] } ] }, "lint-on-stop": { "Stop": [{ "command": "make lint" }] } } ``` **Good** — a tool matcher naming a tool Antigravity has, and a prompt hook that carries no `command`: ```json { "shell-audit": { "PreToolUse": [ { "matcher": "run_command", "hooks": [ { "type": "command", "command": "./scripts/audit-command.sh", "timeout": 5 } ] } ], "PostToolUse": [ { "matcher": "*", "hooks": [{ "command": "./scripts/record-tool-call.sh" }] } ] }, "timetable-reminder": { "PreInvocation": [ { "type": "prompt", "prompt": "Departure times in this repository are UTC.", "model": "gemini-3-pro" } ] } } ``` ## How to fix - Write `timeout` as a whole number of seconds inside the 32-bit range (`5`, not `5.0`, `"5"` or `1099511627776`). - Give a command hook a `command` and nothing else from the prompt side; give a prompt hook a `prompt` and optionally a `model`. - Match against Antigravity's own tool names — `run_command`, `view_file`, `write_to_file`, `replace_file_content`, `browser_*` — and use `""` or `"*"` for every tool. - If the file was written for Claude, Codex or Cursor, move it to that host's directory. Antigravity reads a map of *named* hooks — `{"audit": {"Stop": [...]}}` — not events nested under `hooks`. - Move an `enabled` key inside the named hook it is meant to switch off. - Drop a key the parser discards, or move its value into the `command` itself. If Antigravity adds an event newer than this skillsaw release, allow it in `.skillsaw.yaml`. Extra events accept both grouped and flat handlers: ```yaml rules: antigravity-hooks-valid: extra-events: - SessionEnd ``` ## Configuration ```yaml rules: antigravity-hooks-valid: enabled: auto # true | false | auto severity: error ``` | Parameter | Description | Default | |-----------|-------------|---------| | `extra-events` | Additional hook event names to accept, for events newer than this skillsaw release | `[]` | *Run `skillsaw explain antigravity-hooks-valid` to see this documentation and the rule's effective configuration in your terminal.*