ANTIGRAVITY LABJP
Articles/Antigravity Basics
Antigravity Basics/2026-07-31Advanced

The Flaky Hook Started Failing Every Time — Migrating to Declarative Sessions in the Antigravity SDK

Registering hooks after session init is gone, replaced by declaration at session creation. Here is what happened when I moved four agent definitions across, plus the harness I used to measure ordering non-determinism over 2,000 runs.

Antigravity345SDK5Hook DesignMigration6Reproducibility2

Premium Article

The morning after the migration, CI was stuck on red.

The day before, that same test suite passed maybe four runs out of ten. Now it never passed. The only thing I had touched was how hooks were registered.

Here is the conclusion up front: that red board was not a failed migration. It was proof the migration had worked.

What changed is not what you can register, but when it is decided

In the Antigravity SDK, registering hooks and triggers dynamically after session initialization has been removed in favor of declaring them at session creation. Details shift between versions, so check the current wording in the Antigravity changelog before you plan your own move.

What you can register has not changed. What changed is the moment your configuration becomes final.

AspectDynamic registration after initDeclaration at session creation
When config is finalizedAt runtime, as registrations completeThe instant the session is created
Hook execution orderDepends on which registration finished firstThe order of the array you wrote
Where conditionals liveIn whether you register at allInside the hook body
Reproducibility for identical inputCan vary run to runAlways identical
Inspecting the configurationRequires running itReadable statically, before launch

I thought I was migrating for the last row. Being able to read a configuration without executing it is the obvious win. The row that actually mattered turned out to be the one above it.

Find every call site mechanically

My first instinct was grep -rn "register_hook". I only maintain four agent definitions for my own apps, so eyeballing the results felt reasonable.

It was not enough. A register_hook sitting inside an if, inside a loop, or inside a try demands completely different rewrites. Grep hands you lines. It does not hand you the context those lines sit in.

So I wrote a small script that reports both the call site and its syntactic context.

#!/usr/bin/env python3
"""Find post-init hook/trigger registrations and sort them by migration difficulty."""
import ast, os, sys
 
DYNAMIC = {"register_hook", "register_trigger", "add_hook", "add_trigger"}
 
def scan(root):
    rows = []
    for dp, _, fs in os.walk(root):
        if any(seg in dp for seg in (".venv", "node_modules", "__pycache__")):
            continue
        for f in fs:
            if not f.endswith(".py"):
                continue
            p = os.path.join(dp, f)
            try:
                tree = ast.parse(open(p, encoding="utf-8").read(), p)
            except SyntaxError as e:
                # Half-migrated files will land here. Never swallow this silently.
                print(f"skip (syntax): {p}: {e}", file=sys.stderr)
                continue
 
            # ast nodes carry no parent pointer, so build the child -> parent map first
            parent = {}
            for n in ast.walk(tree):
                for c in ast.iter_child_nodes(n):
                    parent[c] = n
 
            for n in ast.walk(tree):
                if not (isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)):
                    continue
                if n.func.attr not in DYNAMIC:
                    continue
                ctx, cur = set(), parent.get(n)
                while cur is not None:
                    if isinstance(cur, ast.If):
                        ctx.add("conditional")
                    elif isinstance(cur, (ast.For, ast.While, ast.comprehension)):
                        ctx.add("loop")
                    elif isinstance(cur, (ast.Try, ast.ExceptHandler)):
                        ctx.add("try")
                    cur = parent.get(cur)
                rows.append((p, n.lineno, n.func.attr, sorted(ctx)))
    # ast.walk is breadth-first, so results are not in source order. Re-sort here.
    return sorted(rows, key=lambda r: (r[0], r[1]))
 
TIER = {
    frozenset(): "A: move straight into an array",
    frozenset({"conditional"}): "B: rewrite as an early return inside the hook",
    frozenset({"loop"}): "B: expand generated functions into the array",
    frozenset({"try"}): "C: decide failure behavior first",
}
 
def main(root):
    rows = scan(root)
    for p, line, call, ctx in rows:
        tier = TIER.get(frozenset(ctx), "C: decide by hand")
        print(f"{p}:{line}  {call:<17} ctx={','.join(ctx) or '-':<12} {tier}")
    print(f"\n{len(rows)} call sites total")
 
if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else ".")

Because ast.walk traverses breadth-first, printing results as they come out scrambles the file order. Migration work goes top-to-bottom through a file, so skipping that re-sort costs you rework. That tripped me up on the first pass.

Running it against my definitions:

sample/agents/build_agent.py:4  register_hook     ctx=-            A: move straight into an array
sample/agents/build_agent.py:6  register_hook     ctx=conditional  B: rewrite as an early return inside the hook
sample/agents/build_agent.py:7  register_trigger  ctx=-            A: move straight into an array
sample/agents/review_agent.py:5  register_hook     ctx=loop         B: expand generated functions into the array
sample/agents/review_agent.py:6  register_trigger  ctx=-            A: move straight into an array

5 call sites total

Three A's, two B's. Small numbers, but the split is what lets you estimate how far you will get in a sitting. The one tier that needs thought before any code moves is C — a registration inside a try forces you to answer whether a failed registration should halt startup or let it continue.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
An AST-based script that finds every post-init register_hook / register_trigger call and sorts them into difficulty tiers A, B, and C — complete and ready to run against your own repository
A measured result showing that dynamic registration satisfied hook ordering dependencies only 48.2% of the time across 2,000 runs, along with the minimal harness that produced it
The reversal nobody warns you about: declaring hooks does not remove the bug, it makes the bug reproduce every time — and how to read a CI board that turns solid red right after the migration
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Antigravity2026-06-24
Combining All Four Antigravity Surfaces in One Project — Up to Running Your Own SDK Agent
How to split a single project across Antigravity 2.0, CLI, IDE, and SDK, and how to bridge between them — from diverging on design to converging on production, all the way to running a small custom agent with the Python SDK, with implementation included.
Antigravity2026-06-24
Antigravity 2.0, CLI, IDE, SDK — Weaving All Four Surfaces Through a Real Project
Antigravity ships as a desktop app, a CLI, an IDE, and a Python SDK. Beyond picking one, this guide shows how to weave all four across a single project — with a headless-execution wrapper for automation, plus the cost and migration traps to sidestep.
Antigravity2026-05-23
Moving to the Antigravity CLI (agy): Shifting Your Scripts Off Gemini CLI Before the June 18 Shutdown — Without Downtime
A grounded walkthrough of Google's Antigravity CLI (agy): fastest setup, a no-downtime migration off Gemini CLI, a compatibility shim, the Pro vs Ultra cost break-even, and running recurring work with scheduled messages — based on actually moving my own repos across.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →