Hooks
What antigravity runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add yuting0624/antigravity-for-claude-code > /plugin install antigravity@antigravity-for-claude-code
Ships with antigravity. Installing the plugin gets these hooks.
What fires, and when
SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
- Matches
startup"${CLAUDE_PLUGIN_ROOT}/hooks/check-agy.sh" - Matches
startup"${CLAUDE_PLUGIN_ROOT}/hooks/inject-policy.sh" - Matches
compact"${CLAUDE_PLUGIN_ROOT}/hooks/inject-policy.sh"
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
"${CLAUDE_PLUGIN_ROOT}/hooks/nudge-delegation.sh"
Where it lives
- hooks/check-agy.shRunsGitHub
Read the script
#!/usr/bin/env bash # # SessionStart hook: lightweight check that the Antigravity CLI (`agy`) is usable. # Warns on stderr but NEVER fails the session (always exits 0). The full health # check lives in scripts/doctor.sh — this one stays fast (no `agy models` network # call) so it doesn't slow every session start. # set -uo pipefail if ! command -v agy >/dev/null 2>&1; then echo "[antigravity] agy not on PATH — install the Antigravity CLI to enable delegation:" >&2 echo "[antigravity] https://antigravity.google/docs/cli-using" >&2 exit 0 fi if ! agy --version >/dev/null 2>&1; then echo "[antigravity] agy is on PATH but '--version' failed — it may need authentication (run \`agy\` once)." >&2 fi exit 0
- hooks/inject-policy.shRunsGitHub
Read the script
#!/usr/bin/env bash # # SessionStart hook: inject this plugin's COST-AWARE routing policy as session # context, so the discipline (delegate above the break-even, keep Claude's context # lean, always verify agy's output) applies even when the `antigravity` skill isn't # explicitly invoked. Prints the hookSpecificOutput JSON on stdout. # # Toggle off via plugin userConfig `coding_policy` (env CLAUDE_PLUGIN_OPTION_CODING_POLICY: # off / false / 0 / no / disabled). Default: on. # set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" raw="$(printf '%s' "${CLAUDE_PLUGIN_OPTION_CODING_POLICY:-on}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" case "$raw" in off|false|0|no|disabled) exit 0 ;; esac cat "$HERE/policy-context.json" exit 0 - hooks/nudge-delegation.shRunsGitHub
Read the script
#!/usr/bin/env bash # # UserPromptSubmit hook: a cheap, deterministic nudge toward delegation when the # user's prompt LOOKS like bulk work above the delegation break-even. # # Design principle: this supplies judgment MATERIAL — the DECISION stays with # Claude (per the skill's cost discipline). It never forces a delegation and it # never fires the wrapper itself: full automation is a measured net loss below # the break-even, so the break-even call must remain a per-task judgment. # # Heuristic is deliberately conservative (volume/fan-out phrases, EN + JA), and # the nudge text is a FIXED string — the user's prompt is never echoed back into # the context (no escaping/injection surface). # # Toggle via plugin userConfig `delegation_nudge` # (env CLAUDE_PLUGIN_OPTION_DELEGATION_NUDGE: off/false/0/no/disabled). Default: on. # set -uo pipefail raw="$(printf '%s' "${CLAUDE_PLUGIN_OPTION_DELEGATION_NUDGE:-on}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" case "$raw" in off|false|0|no|disabled) exit 0 ;; esac IN="$(cat 2>/dev/null || true)" [ -n "$IN" ] || exit 0 # Extract ONLY the prompt field (matching on the whole payload would false-positive # on cwd/paths). python3 is already a plugin dependency (measure-session, agy-trace). PROMPT="$(printf '%s' "$IN" | python3 -c 'import json,sys try: print(json.load(sys.stdin).get("prompt","")) except Exception: pass' 2>/dev/null || true)" [ -n "$PROMPT" ] || exit 0 # Already delegating explicitly? Stay quiet. case "$PROMPT" in *antigravity*|*agy-delegate*|*agy-job*) exit 0 ;; esac shopt -s nocasematch HIT=0 case "$PROMPT" in *"all files"*|*"every file"*|*"across the codebase"*|*"entire codebase"*|*"whole repo"*| \ *migrate*|*migration*|*"generate tests"*|*"test coverage"*|*"exhaustive test"*| \ *scaffold*|*boilerplate*|*"deep research"*|*"web search"*| \ *一括*|*全ファイル*|*すべてのファイル*|*網羅*|*移行*|*大量*|*横断*|*リポジトリ全体*) HIT=1 ;; esac shopt -u nocasematch [ "$HIT" -eq 1 ] || exit 0 # Fixed nudge. Note the explicit "the judgment is yours" — this is material, not a mandate. cat <<'JSON' {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"[antigravity plugin] This prompt looks like BULK work (mass edits / migration / exhaustive tests / fan-out search) — possibly above the delegation break-even. CONSIDER routing the bulk part to the antigravity-delegate subagent (or agy-delegate --digest) so it runs on the cheap executor, then verify its digest. THE JUDGMENT IS YOURS: if the task is actually small, self-contained, or judgement-heavy, do it yourself — delegating below the break-even is a measured net loss. Decide silently; don't mention this notice."}} JSON exit 0 - hooks/validate-delegate-bash.shGitHub
Read the script
#!/usr/bin/env bash # # PreToolUse(Bash) gate for the `antigravity-delegate` subagent. Claude Code's # subagent `tools:` field can't scope Bash to one command, so this hook is the ONLY # thing restricting what that subagent may run — it must allow a Bash call only when # it invokes the plugin's delegation wrapper (agy-delegate / agy-job) and nothing else. # # Hardening (issue #29): the previous version matched the wrapper name as a SUBSTRING # anywhere in the command, so payloads like `foo ... # agy-delegate` or # `echo $(...) agy-job` slipped through (arbitrary execution under prompt injection). # This version instead: # * requires the FIRST command token (argv[0], basename, optional .sh) to be exactly # agy-delegate / agy-job — a token check, not a substring match; # * allows NO pipeline at all. A producer allowlist was the second half of the #29 # hardening and it is what GHSA-hwv2-vjgj-8rcv broke, twice over: `git` with # unrestricted arguments is a living-off-the-land binary (`git -c alias.x='!cmd' x`, # `--exec-path=`, `-c core.pager=` all execute arbitrary commands, all measured), and # `cat`/`echo`/`printf` feeding `agy-delegate -` reads any file or `$VAR` and ships it # to the external model. Allowing a command by NAME while ignoring its arguments is # not an identity check. Nothing needed the pipeline: this subagent's own contract # says the gate "blocks every Bash command except the delegation wrapper" and shows # only `agy-delegate [options] "<task>"`, and the documented # `git diff | agy-delegate --tier pro -` in commands/review.md runs as the MAIN # Claude, which this hook does not gate; # * rejects UNQUOTED shell metacharacters bash would act on (`; & | < > ( ) #`, # backticks, `$(`, and a NEWLINE — it separates commands just like `;`), while # permitting them INSIDE a quoted prompt (no false positives on legitimate # prompts — command substitution inside double quotes is still blocked because # bash would expand it). Leading/trailing whitespace is stripped first, so a # trailing newline is fine; an internal one is two commands and stays blocked; # * fails CLOSED (block) if the JSON is unparseable or python3 is unavailable. # # On a block it prints the SPECIFIC reason to stderr before the generic message # (issue #51). Claude Code feeds PreToolUse stderr back to the agent, so the caller # can tell "you left a newline in" apart from "you tried to run something else" — # previously both produced the same string and the agent retried the same shape. # # Input: hook JSON on stdin, with .tool_input.command holding the bash command. # Exit: 0 = allow, 2 = block. # set -uo pipefail input="$(cat)" BLOCK_MSG="[antigravity-delegate] blocked: this subagent may only run agy-delegate / agy-job, as a BARE name with no path and no pipeline. No other commands, pipes, chaining, redirection, substitution, comments, or unquoted newlines. To give agy a repository, pass --dir <repo-root> rather than piping content in. Delegate file work to agy; verification is the caller's job." # python3 gives a correct, quote-aware parse. Fail CLOSED if it's missing. if ! command -v python3 >/dev/null 2>&1; then echo "$BLOCK_MSG (python3 unavailable — failing closed)" >&2 exit 2 fi if AGY_GATE_INPUT="$input" python3 - <<'PY' import json, os, shlex, sys raw = os.environ.get("AGY_GATE_INPUT", "") try: cmd = json.loads(raw).get("tool_input", {}).get("command", "") except Exception: sys.exit(2) # unparseable payload -> fail closed if not isinstance(cmd, str) or not cmd.strip(): sys.exit(2) # Leading/trailing whitespace is normalised away BEFORE scanning (issue #51). bash # ignores it, so this cannot change what the command does — and a newline with # nothing after it cannot begin a second command. Internal newlines are untouched # and still rejected below. An unterminated quote still fails the state check: # `agy-delegate "hi\n` strips to `agy-delegate "hi`, which is still unbalanced. cmd = cmd.strip() WRAPPERS = {"agy-delegate", "agy-job"} # Say WHY, on stderr, so the caller can self-correct (issue #51). Claude Code feeds # PreToolUse stderr back to the agent, which is the same path BLOCK_MSG already takes. # # NEVER include the command text. This lands in the agent's context and the blocked # command routinely carries a delegation prompt the caller would not want quoted back; # a character name and an offset are enough to fix it. `argv[0]` is the exception — # it is a command name, not content, and naming it is most of the diagnostic value. def deny(reason): sys.stderr.write("[antigravity-delegate] reason: %s\n" % reason) sys.exit(2) CHAR_NAMES = {";": "';' (command separator)", "&": "'&' (background / chaining)", "<": "'<' (redirection)", ">": "'>' (redirection)", "(": "'(' (subshell)", ")": "')' (subshell)", "#": "'#' (comment)"} # A bare NAME, never a path. This used to be os.path.basename(), which accepted any # directory: `./agy-delegate` in a cloned repository passed the gate and ran THAT file. # Untrusted repository content is the exact prompt-injection source SECURITY.md names, # so the one control it names was defeated by the checkout it was meant to survive. # # Nothing needs a path. agents/, commands/ and skills/ have invoked these by bare name # since 0.14.0 — the plugin puts bin/ on the Bash tool PATH because $CLAUDE_PLUGIN_ROOT # is not exported to model-run Bash (issue #11). A bare name resolves through PATH, # which the working directory is not on; a path resolves through the working directory, # which an attacker controls. # # Returns None for anything path-shaped, which every caller treats as "not allowed". def base(tok): if "/" in tok or "\\" in tok: # backslash too: Git Bash / MSYS accept `.\name` return None return tok[:-3] if tok.endswith(".sh") else tok # Quote-aware scan: split into pi
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Run the Antigravity CLI (Gemini) as a collaborating sub-agent, right inside Claude Code. Claude conducts the judgement; Gemini does the heavy lifting — intelligent model routing across the SDLC.

