A Developer's Guide to Agent Hooks in Antigravity CLI

Gists

Core operational flow of the Antigravity CLI hooks subsystem, detailing lifecycle interception, out-of-band validation, and the execution response cycle.

Motivation

To be quite honest, “Hooks”—the shell commands we trigger at specific points when generative AI agents process tasks—were something I used blindly for a long time. Whenever colleagues asked me about them, I realized I lacked any real confidence in explaining how they actually work. However, when I migrated from Gemini CLI to the new Antigravity CLI, I noticed that the hooks system carried over. This felt like the right moment to stop guessing and finally develop a precise, deep understanding of the mechanism. I went back to the basics to analyze exactly how hooks operate under the hood and how we can use them effectively in the Antigravity environment. My goal is to demystify hooks so we can write them with confidence, and if this guide proves useful to your own workflows as well, I would be very glad.

Abstract

As autonomous AI coding agents transition from sandbox environments to production workspaces, establishing robust, deterministic execution guardrails becomes paramount. This paper analyzes the architectural mechanics of agent lifecycle hooks, tracking their execution flow from legacy open-source implementations to the modern Go-based Google Antigravity CLI. We dissect the hook lifecycle, including input generation, regular expression matching, and sequential result aggregation. Furthermore, we provide concrete, production-ready Shell and Node.js implementation patterns for blocking dangerous shell command executions and regulating Model Context Protocol (MCP) tool dispatches, establishing deterministic security guardrails for terminal-first autonomous developers.

Introduction

In the summer of 2025, Anthropic introduced “Hooks” into its terminal agent lifecycle Ref. This development represented a shift from relying on LLM decision-making to using deterministic execution boundaries. Rather than trying to persuade an agent to avoid destructive actions, hooks enforce hard, programmatically defined rules at key lifecycle events.

This technical framework was subsequently adopted and refined by Google within its developer agent ecosystems, culminating in the release of the Go-based Antigravity CLI Ref. To build secure, high-throughput autonomous development workflows, engineers must move away from soft prompting and adopt strict, runtime-level interception. This paper analyzes the internal mechanics of these hook architectures, examines the transition from legacy setups to modern engines, and offers reference implementations for production guardrails.

Architectural Blueprint of Lifecycle Interception: Lessons from Gemini CLI

To construct reliable guardrails, we must first analyze the execution loop of the hook subsystem. By examining the structural designs preserved from the open-source Gemini CLI, we can map the exact routing of context, decision-making, and output aggregation.

fig1b Mermaid Chart Playground

The hook subsystem is split into dedicated, object-oriented modules that isolate registration, matching, running, and aggregation:

Component Source File Responsibility
HookSystem hookSystem.ts The main facade acting as the entry point. It exposes high-level event methods and coordinates the lower-level sub-modules.
HookRegistry hookRegistry.ts Discovers, validates, and stores hooks declared in system, user, and project-level settings, maintaining their execution priorities.
HookPlanner hookPlanner.ts Resolves target context against user-defined regular expressions (matchers) to construct a deduplicated, ordered execution plan.
HookRunner hookRunner.ts Executes the resolved plan, spawning out-of-band shell processes (Command Hooks) or executing JS/TS modules (Runtime Hooks).
HookAggregator hookAggregator.ts Merges outcomes from multiple concurrent or serial hooks into a single consolidated decision structure using event-specific logic.
HookEventHandler hookEventHandler.ts Constructs the base context payload (HookInput), manages telemetry, and triggers the orchestrator when agent lifecycle shifts occur.

The sequential interaction between these core classes during an interception loop is detailed below:

fig1c Mermaid Chart Playground

Detailed Hook Pipeline Mechanics

1. Context Object Generation (HookInput)

Upon event detection, the system initiates HookEventHandler.createBaseInput(), assembling a standardized, environment-aware context payload:

2. Pattern Matching and Deduplication

HookPlanner evaluates the user’s matcher parameters:

3. Execution Topology and State Chaining

4. Subprocess Isolation and Output Handling

The runner handles external commands using HookRunner.executeCommandHook() under strict operational parameters:

5. Output Aggregation

HookAggregator processes multiple execution results based on the event context:


A Concrete Execution Scenario: The Lifecycle of a File Write Request

To illustrate these components in action, consider a local agent processing a file modification request under active hook surveillance:

fig1d Mermaid Chart Playground

  1. BeforeAgent Execution: The user’s prompt is processed by inject-time.js. The script outputs the current timestamp via additionalContext, which is merged into the LLM prompt. The model processes the instructions and decides to call the write_file tool.
  2. BeforeTool Verification: Before the tool runs, security-check.sh receives the tool argument payload: { "path": "temp.txt", "content": "Hello World" }. It verifies that the path lies within safe workspace limits and returns { "decision": "allow" }.
  3. Execution & Finalization: The file write completes. The agent loop processes the tool output, generates a final response, and triggers notification.sh via the AfterAgent event. This script issues a desktop alert to notify the developer before the terminal renders the final response.

Lifecycle Hook Events Directory

Event Name Timing Intercept/Block (decision) Support Purpose and Key Use Cases
SessionStart At session start, resume, or clear No Initialize resources, pre-load domain context, display welcome messages.
SessionEnd At session close or exit No Resource cleanup, session logging, pushing metrics to telemetry endpoints.
BeforeAgent Immediately after user input, before planning Yes User prompt validation, dynamic injection of active environment context (e.g., git branch, workspace state), blocking malicious prompts.
AfterAgent Once the agent loop completes a turn Yes Auditing output, forcing retries (decision: 'deny'), or clearing context via clearContext: true.
BeforeModel Immediately prior to dispatching an LLM request Yes Modifying systemic instructions, replacing models dynamically, mocking responses to bypass API consumption.
AfterModel Upon receiving the model response (per stream chunk) Yes Content filtering, redacting sensitive parameters, scanning for API tokens before user display.
BeforeToolSelection Prior to the LLM determining tool configurations No (Tool-filtering only) Dynamically limiting or altering the set of active functions (overwriting toolConfig).
BeforeTool Immediately before tool dispatch Yes Intercepting execution arguments, blocking high-risk commands, or rewriting input values on-the-fly.
AfterTool Immediately post-tool execution Yes Modifying tool output streams, hiding error trace logs, or executing auxiliary trailing commands (tailToolCallRequest).
PreCompress Immediately before summarizing context history No Saving raw history pre-summarization, notifying the developer of context reduction.
Notification Triggered by wait-states (e.g., waiting for user permission) No Monitoring & Notification Only. Broadcasting active wait status or blocking loops to messaging APIs (Slack, Discord) or desktop notifications.

Evolutionary Jump: Porting Hooks to the Antigravity CLI

With the transition to the Antigravity CLI (agy), the execution pipeline was optimized for speed and multi-agent coordination Ref. The extensive list of eleven legacy hooks was consolidated into five core, high-efficiency checkpoints.

The Five Antigravity Hook Events

Event Name Matcher Support Description & Trigger Timing
PreToolUse Yes (Regex supported) Fires immediately before tool dispatch. Uses the matcher parameter to isolate target commands or APIs (e.g., "run_command"). Primarily used to intercept and block unsafe operations.
PostToolUse Yes (Regex supported) Fires immediately after a tool completes execution. Useful for cleanup, format linting, or telemetry tracking.
PreInvocation No Intercepts execution immediately prior to an LLM invocation. Typically used to append workspace context or security policies to system prompts.
PostInvocation No Evaluates response payloads immediately after receiving data from the model. Useful for auditing or scanning responses for secret leaks.
Stop No Triggers when the agent finishes its plan and prepares to exit the active session. Typically used for workspace cleanup and session logging.

Valid Matcher Profiles (for PreToolUse and PostToolUse)

For tool-level interception, the matcher setting accepts a regular expression to target specific agent capabilities:


Environment Configuration and Integration Setup

To use hooks in your development environment, ensure you meet the following prerequisites:

  1. Google Antigravity CLI (agy) must be installed and authenticated via your Google account or enterprise GCP project.
  2. The target workspace must be verified and trusted in your client settings.

Configuration Scopes

Note: Project-local configuration files are evaluated at runtime by the agent loop, but they may not appear within the /hooks interactive CLI panel in early releases.


Critical Constraints, Lifecycle Integration, and Failure Modes

When designing hooks for the Antigravity engine, several critical constraints must be addressed to prevent security bypasses or runtime crashes:

1. Scope and Veto Policies

If a hook is declared in both global and project-level scopes, both hooks will run. If any script returns allow_tool: false, the operation is blocked. This veto-power design ensures that global organization-wide security rules cannot be bypassed by project-level settings.

2. Timeout Hazards

While you can customize execution timeouts in the configuration (e.g., "timeout": 30), setting a timeout to 0 will immediately kill the subprocess. This makes the hook fail and can halt agent execution. Always specify a reasonable duration (typically between 15 and 60 seconds).

3. Absolute Path Constraints

The command parameter in hooks.json must use an absolute path (e.g., /home/user/project/.agents/hooks/script.sh). Relative paths resolve against the active working directory of the terminal where agy was launched. If you start a session from a subfolder, relative paths will fail with an exit status 127 (command not found) error, bypassing the security guardrail.

4. JSON Payload Interception in Go

The Go-based agy runtime pipes arguments to standard input (stdin) using a nested structure. For shell command executions, arguments are located under .toolCall.args.CommandLine. Legacy paths like .arguments.CommandLine are incompatible. Additionally:

Execution logic of a resilient shell interceptor hook, illustrating fallback parsing mechanisms and standard output formatting.

Production-Ready Code Blueprint Samples

This section provides concrete, system-tested code blueprints for implementing custom security guardrails using the Google Antigravity hooks engine. These scripts and configuration layouts establish programmatic boundaries around high-risk system calls and Model Context Protocol (MCP) tool integrations.


Sample 1: Restricting Shell Execution with a Static PreToolUse Barrier

This implementation establishes an absolute barrier against executing any shell commands inside the local workspace directory.

1. Hook Script (/absolute/path/to/project/.agents/hooks/deny-run-command.sh)

Create this file and save it within your workspace environment:

#!/bin/bash
# Read standard input to clear the pipeline buffer
input_json=$(cat)

# Emit top-level JSON indicating execution rejection
cat <<EOF
{
  "allow_tool": false,
  "deny_reason": "Executing shell commands is strictly prohibited in this project for security reasons."
}
EOF

# Exit with success status to ensure the decision engine evaluates the JSON
exit 0

Ensure the script is flagged as executable:

chmod +x /absolute/path/to/project/.agents/hooks/deny-run-command.sh

2. Configuration (hooks.json)

Register the script inside your workspace-level hook configuration file (<project_root>/.agents/hooks.json):

{
  "block-run-command": {
    "PreToolUse": [
      {
        "matcher": "run_command",
        "hooks": [
          {
            "type": "command",
            "command": "/absolute/path/to/project/.agents/hooks/deny-run-command.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

3. Operational Testing and Execution Results

To test the barrier, restart the agy session and verify that the hook is active using the /hooks command. Next, issue a prompt that triggers shell command execution.

User Input

Show me the git status of this project using a command.

Engine Action

fig2a

The agent attempts to call the run_command tool to run git status. The PreToolUse hook intercepts the execution request and passes the event payload to the script. The script returns allow_tool: false.

Terminal Interface (TUI) Output

Error invalid tool call: Tool call denied with reason: Executing shell commands is strictly prohibited in this project for security reasons.

Agent Behavior The agent registers the tool rejection and recognizes that shell command execution is blocked by system policy. It halts further execution attempts and informs the user of the security constraint.


Sample 2: Dynamically Inspecting Execution Lines for Selective Command Filtering

This implementation selectively filters command payloads, blocking dangerous utilities while allowing standard developer queries (like ls or git status).

1. Hook Script (/absolute/path/to/project/.agents/hooks/filter-run-command.sh)

Create this script to inspect command line parameters before execution:

#!/bin/bash
# Read the stdin JSON payload piped from the agent loop
input_json=$(cat)

# Extract execution arguments, falling back to basic parsing if jq is absent
if [ -x /usr/bin/jq ]; then
  command_line=$(echo "$input_json" | /usr/bin/jq -r '.toolCall.args.CommandLine')
elif command -v jq >/dev/null 2>&1; then
  command_line=$(echo "$input_json" | jq -r '.toolCall.args.CommandLine')
else
  # RegEx string-matching fallback
  command_line=$(echo "$input_json" | grep -oE '"CommandLine"\s*:\s*"[^"]*"' | head -n 1 | cut -d'"' -f4)
fi

# Define policy parameters and verify execution strings
is_blocked=false
blocked_reason=""

if echo "$command_line" | grep -qE '\b(rm|curl|wget|shutdown|reboot|poweroff)\b'; then
  is_blocked=true
  blocked_reason="The command contains a restricted utility (rm, curl, wget, shutdown, reboot, or poweroff) which is blocked by security policy."
fi

# Format output based on validation state
if [ "$is_blocked" = true ]; then
  cat <<EOF
{
  "allow_tool": false,
  "deny_reason": "$blocked_reason"
}
EOF
else
  cat <<EOF
{
  "allow_tool": true
}
EOF
fi

exit 0

2. Configuration (hooks.json)

Add the selective command filter to your hooks.json file:

{
  "filter-run-command": {
    "PreToolUse": [
      {
        "matcher": "run_command",
        "hooks": [
          {
            "type": "command",
            "command": "/absolute/path/to/project/.agents/hooks/filter-run-command.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

3. Operational Testing and Execution Results

Case A: Permitted Commands

User Input

Show me the git status of this project using a command.

Engine Action

fig2b

The agent attempts to execute git status. The hook script checks the input parameters, finds no prohibited patterns, and returns { "allow_tool": true }.

Terminal Interface (TUI) Output The command executes normally, and the repository status is rendered directly in the console.

Case B: Restricted Commands and Agent Pivoting

User Input

Download a test file from http://example.com using a command.

Engine Action The agent attempts to invoke curl http://example.com. The hook intercepts the command, detects the prohibited keyword curl, and rejects the operation.

Terminal Interface (TUI) Output

⚠ Tool call denied by jsonhook__filter-run-command_PreToolUse_0_0: The command contains a restricted utility (rm, curl, wget, shutdown, reboot, or poweroff) which is blocked by security policy.

Agent Pivoting Behavior Because the hook returns a specific rejection reason, the agent parses the warning and adapts its strategy. Knowing that curl is blocked, it writes a Python alternative using standard library calls to execute the download safely:

● Bash (python3 -c "import urllib.request; urllib.request.urlretrieve('http://example.com', 'example.html')")

This alternative command bypasses the forbidden tool list and runs successfully.


Sample 3: Regulating Model Context Protocol (MCP) Tool Execution

This implementation applies security policies to external tools integrated via the Model Context Protocol (MCP).

1. Target MCP Server (/absolute/path/to/project/.agents/mcp-server/mcp-server.js)

Create a mock Node.js MCP server that handles basic input and output:

const readline = require("readline");

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false,
});

rl.on("line", (line) => {
  try {
    const request = JSON.parse(line);

    // Process discovery requests from the agent client
    if (request.method === "tools/list") {
      const response = {
        jsonrpc: "2.0",
        id: request.id,
        result: {
          tools: [
            {
              name: "hello_world",
              description: "A simple hello world tester.",
              inputSchema: {
                type: "object",
                properties: {
                  greeting: { type: "string" },
                },
                required: ["greeting"],
              },
            },
          ],
        },
      };
      console.log(JSON.stringify(response));
    } else if (request.method === "tools/call") {
      // Execute the requested tool action
      const response = {
        jsonrpc: "2.0",
        id: request.id,
        result: {
          content: [
            {
              type: "text",
              text: `Hello, ${request.params.arguments.greeting}!`,
            },
          ],
        },
      };
      console.log(JSON.stringify(response));
    }
  } catch (err) {
    // Gracefully ignore malformed inputs
  }
});

2. Server Registry Setup (mcp_config.json)

Configure your server within the agent’s MCP settings file (<project_root>/.agents/mcp_config.json):

{
  "mcpServers": {
    "test-server": {
      "command": "node",
      "args": ["/absolute/path/to/project/.agents/mcp-server/mcp-server.js"]
    }
  }
}

3. MCP Interceptor Script (/absolute/path/to/project/.agents/hooks/deny-mcp-tool.sh)

Create a script to inspect and authorize MCP tool calls:

#!/bin/bash
# Read the piped stdin JSON payload containing the MCP request context
input_json=$(cat)

# Extract the tool name parameter safely
if [ -x /usr/bin/jq ]; then
  tool_name=$(echo "$input_json" | /usr/bin/jq -r '.toolCall.args.ToolName // .toolCall.args.toolName')
elif command -v jq >/dev/null 2>&1; then
  tool_name=$(echo "$input_json" | jq -r '.toolCall.args.ToolName // .toolCall.args.toolName')
else
  // Fallback parameter extraction
  tool_name=$(echo "$input_json" | grep -oE '"[Tt]oolName"\s*:\s*"[^"]*"' | head -n 1 | cut -d'"' -f4)
fi

# Apply security policies to specific MCP tools
if [ "$tool_name" = "hello_world" ]; then
  cat <<EOF
{
  "allow_tool": false,
  "deny_reason": "Execution of the MCP tool 'hello_world' is blocked by project policy."
}
EOF
else
  cat <<EOF
{
  "allow_tool": true
}
EOF
fi

exit 0

4. Configuration (hooks.json)

Register the MCP interceptor by targeting call_mcp_tool in your config file:

{
  "deny-mcp-tool": {
    "PreToolUse": [
      {
        "matcher": "call_mcp_tool",
        "hooks": [
          {
            "type": "command",
            "command": "/absolute/path/to/project/.agents/hooks/deny-mcp-tool.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

5. Operational Testing and Execution Results

User Input

Call the hello_world tool of the test-server MCP server with greeting 'Antigravity'.

Engine Action

fig2c

The agent attempts to execute the hello_world tool via its registered call_mcp_tool dispatcher. The PreToolUse hook intercepts the execution request, identifies the targeted tool as hello_world, and rejects the operation.

Terminal Interface (TUI) Output

⚠ Tool call denied by jsonhook__deny-mcp-tool_PreToolUse_0_0: Execution of the MCP tool 'hello_world' is blocked by project policy.

Agent Behavior The agent registers the denial, understands that running the hello_world tool violates local security settings, and notifies the developer that the operation was blocked.


Deepening Control: Advanced Integration and the Evolution of Hooks

As agent environments become more complex, hooks are evolving from simple script execution checkers into distributed validation pipelines.

Multi-Device Approvals and Asynchronous Telemetry

A prominent example of this evolution is the integration of hooks with “Agent Approve” workflows Ref. Under this paradigm:

Topology of a multi-device, synchronous verification loop integrating local Antigravity CLI hooks with external mobile security interfaces.

Context Injection and Safe Chaining

Beyond security checks, PreInvocation and PreToolUse hooks can be chained to inject dynamic context into the agent’s prompt context:

This layered architecture combines dynamic context injection with strict, out-of-band security rules, allowing developers to safely delegate complex tasks to autonomous agents.

Implementing Lifecycle Hooks in Sandboxed Environments: The GASADK (adk-gas) Framework

Integrating a lifecycle hook architecture within restricted serverless runtime environments, such as Google Apps Script (GAS), requires adapting the standard design patterns of native command-line engines. In GASADK (adk-gas) Ref—an autonomous agent development kit designed for the GAS ecosystem—the local subprocess spawning model used by native CLI environments has been successfully rewritten and implemented to work reliably within the security and execution limits of the Google cloud sandbox.

Google Apps Script Platform Constraints and Mitigations

1. Inability to Spawn Native Subprocesses

Standard agent engines intercept commands by executing a localized spawn call to run external shell scripts on the host system. This mechanism is structurally impossible within the V8-based Google Apps Script runtime.

2. The Absolute Six-Minute Execution Constraint

Standard consumer GAS executions are capped at six minutes. Integrating heavy, synchronous security checks or network requests risks exhausting the main thread’s time quota and causing execution failures.


GASADK Hook Architecture and Configuration

The hook interface and setup configurations are represented in the TypeScript definitions below, showing how standard pattern matchers and handlers are adapted for GAS-compliant execution:

// TypeScript interface definition for GASADK lifecycle hooks
interface GasHookConfig {
  name: string;
  type: "gas_function" | "webhook";
  // Applicable when type is 'gas_function': the identifier of a function in the global GAS scope
  functionName?: string;
  // Applicable when type is 'webhook': the destination URL processed via UrlFetchApp
  url?: string;
  timeout?: number; // Execution time limit represented in milliseconds
  matcher?: string; // Regular expression parsed against targeted tool identifiers
}

// Reference execution configuration for the autonomous agent sandbox
const adkSettings = {
  hooks: {
    BeforeTool: [
      {
        name: "SecurityGuard",
        type: "gas_function",
        functionName: "checkToolSafety",
        matcher: "GoogleDriveApp.*|GmailApp.*",
      },
    ],
    Notification: [
      {
        name: "SlackNotifier",
        type: "webhook",
        url: "https://hooks.slack.com/services/...",
      },
    ],
  },
};

Practical Use Cases for Hooks in GASADK

With the core hooks engine fully integrated and functional inside GASADK Ref, developers can deploy automated agent configurations tailored to cloud workspaces. The following operational use cases demonstrate the programmatic capabilities of this implementation.

Use Case 1: Safety Guardrails for Google Workspace Operations (PreToolUse)

Use Case 2: Auto-Saving State to Prevent the Six-Minute Execution Boundary (PreCompress / SessionEnd / Notification)

Use Case 3: Automatic Compliance Audit Logging (BeforeTool / AfterTool)

Use Case 4: Real-Time Dynamic Context Injection (SessionStart / BeforeAgent)

Use Case 5: Compliance and Sensitive Data Censorship (AfterAgent)

Summary

Through this study, we achieved three primary goals: we demystified the internal mechanics of agent hooks, made them fully functional in Google’s Go-based Antigravity CLI Ref, and successfully implemented this framework into GASADK Ref. Analyzing the legacy Gemini CLI codebase allowed us to build a precise conceptual model of execution loops, input mapping, and decision aggregation. This knowledge enabled us to construct robust local CLI security boundaries and build serverless-ready hook configurations. Developing these guardrails transitions autonomous developer systems from loose, prompt-based guidelines to absolute, deterministic safety gates.


Acknowledgement

 Share!