Skip to content
Development
Hook

Hooks

What antigravity runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
antigravity
3572 skills1 agent10 commands2 hooks
Install
> /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.

  • Matchesstartup"${CLAUDE_PLUGIN_ROOT}/hooks/check-agy.sh"
  • Matchesstartup"${CLAUDE_PLUGIN_ROOT}/hooks/inject-policy.sh"
  • Matchescompact"${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"
Read hooks/hooks.json

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.

Ships withantigravity

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.

Get the whole plugin
Stats
360
Stars
33
Forks
Active
Maintenance
Shell
Language
MIT
License
3d ago
Last commit
3mo ago
Created

Repo: yuting0624/antigravity-for-claude-code