Skip to content

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

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/<name>/ 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 <path> 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 and can be inventoried against an allowlist with hooks-prohibited.

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:

{
  "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:

{
  "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:

rules:
  antigravity-hooks-valid:
    extra-events:
      - SessionEnd

Configuration

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.