Gemini CLI Hook System: Exhaustive Reference
This document provides an exhaustive reference for the Gemini CLI hook system, covering configuration, JSON schemas, tool names, edge cases, version differences, and comparisons with Claude Code.
Overview
Gemini CLI Hook System: Exhaustive Reference
This document provides an exhaustive reference for the Gemini CLI hook system, covering configuration, JSON schemas, tool names, edge cases, version differences, and comparisons with Claude Code.
Last Updated: January 2026 Gemini CLI Versions Covered: v0.15.x through v0.23.x
Table of Contents
- Overview
- Settings File Locations
- Hook Configuration JSON Schema
- Hook Events
- Tool Names
- Input JSON Schema
- Output JSON Schema
- Exit Code Behavior
- Decision Field Values
- Matcher Patterns
- Configuration Merge Strategies
- Environment Variables
- CLI Commands for Hook Management
- Claude Code Migration
- Gemini CLI vs Claude Code Comparison
- Version History and Changes
- Known Bugs and Issues
- Edge Cases and Quirks
- Best Practices
- Security Considerations
- Sources and References
Overview
Gemini CLI hooks are scripts or programs executed at specific points in the agentic loop, allowing interception and customization of behavior without modifying CLI source code.
Key Characteristics:
- Hooks run synchronously within the agent loop
- Communication uses JSON over stdin/stdout with exit code signaling
- Hooks mirror the contract used by Claude Code for compatibility
- The system was developed starting in late 2024 and matured through 2025
Sources:
Settings File Locations
Configuration Hierarchy (Lowest to Highest Precedence)
| Priority | Location | Description |
|---|---|---|
| 1 | Hardcoded defaults | Built into Gemini CLI |
| 2 | System defaults | OS-specific location (see below) |
| 3 | User settings | ~/.gemini/settings.json |
| 4 | Project settings | .gemini/settings.json in project root |
| 5 | System overrides | /etc/gemini-cli/settings.json |
| 6 | Environment variables | Runtime overrides |
| 7 | Command-line arguments | Highest priority |
System Default Locations by OS
| OS | Path |
|---|---|
| Linux | /etc/gemini-cli/system-defaults.json |
| macOS | /Library/Application Support/GeminiCli/system-defaults.json |
| Windows | C:\ProgramData\gemini-cli\system-defaults.json |
The path can be overridden using GEMINI_CLI_SYSTEM_DEFAULTS_PATH environment variable.
Format Migration (September 2025)
- 09/10/25: New nested JSON format supported in stable release
- 09/17/25: Automatic migration from old format began
- Legacy v1 configuration documented separately
Source: Gemini CLI Configuration
Hook Configuration JSON Schema
Basic Structure
{
"hooks": {
"EventName": [
{
"matcher": "pattern",
"hooks": [
{
"name": "unique-hook-identifier",
"type": "command",
"command": "./path/to/script.sh",
"description": "Human-readable description",
"timeout": 30000
}
]
}
],
"disabled": ["hook-name-1", "hook-name-2"]
}
}
Hook Object Properties
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| name | string | Recommended | Command path | Unique identifier for enable/disable commands |
| type | string | Yes | - | Hook type; currently only "command" supported |
| command | string | Yes | - | Path to script; supports $GEMINI_PROJECT_DIR |
| description | string | No | - | Shown in /hooks panel |
| timeout | number | No | 60000 | Timeout in milliseconds |
| matcher | string | No | - | Pattern to filter when hook runs |
Extension Hooks (v0.21.0+)
Extensions can include a hooks/hooks.json file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "WriteFile",
"hooks": [
{
"type": "command",
"command": "${extensionPath}/scripts/lint.sh"
}
]
}
]
}
}
Extension hooks support ${extensionPath} variable substitution.
Source: GitHub Issue #14449: Hook Support in Extensions
Hook Events
Complete Event List
| Event | Trigger Point | Matcher Applies To |
|---|---|---|
| SessionStart | Session initialization | startup, resume, clear |
| SessionEnd | Session termination | exit, clear, logout, prompt_input_exit, other |
| BeforeAgent | Before planning phase | N/A |
| AfterAgent | After agent loop completes | N/A |
| BeforeModel | Before LLM request | N/A |
| AfterModel | After LLM response | N/A |
| BeforeToolSelection | Pre-tool filtering | N/A |
| BeforeTool | Before tool execution | Tool names |
| AfterTool | After tool execution | Tool names |
| PreCompress | Before context compression | manual, auto |
| Notification | Permission/notification events | ToolPermission |
Event Capabilities
| Event | Can Block | Can Modify Input | Can Inject Context |
|---|---|---|---|
| BeforeTool | Yes | Yes (via deny + reason) | Yes |
| AfterTool | Yes | N/A | Yes (additionalContext) |
| BeforeAgent | Yes | N/A | Yes (additionalContext) |
| AfterAgent | Yes | N/A | N/A |
| BeforeModel | Yes | Yes (llm_request) | N/A |
| AfterModel | Yes | Yes (llm_response) | N/A |
| BeforeToolSelection | No | Yes (toolConfig) | N/A |
| SessionStart | No | N/A | Yes (additionalContext) |
Source: Hooks Reference
Tool Names
Core Tool Names for Matchers
| Category | Tool Names |
|---|---|
| File Operations | read_file, read_many_files, write_file, replace |
| File System | list_directory, glob, search_file_content |
| Shell Execution | run_shell_command |
| Web/External | google_web_search, web_fetch |
| Agent Features | write_todos, save_memory, delegate_to_agent |
MCP Tool Name Format
MCP tools follow the pattern: mcp__<server_name>__<tool_name>
Example: mcp__github__create_issue
Shell Tool Details (run_shell_command)
The shell tool executes commands with platform-specific behavior:
| Platform | Execution Method |
|---|---|
| Windows | powershell.exe -NoProfile -Command |
| Linux/macOS | bash -c |
Configuration options:
{
"tools": {
"shell": {
"enableInteractiveShell": true,
"showColor": true,
"pager": "cat"
}
}
}
When run_shell_command executes, it sets GEMINI_CLI=1 environment variable in subprocesses.
Source: Shell Tool Documentation
Input JSON Schema
Base Fields (All Events)
Every hook receives these fields via stdin:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/path/to/project",
"hook_event_name": "BeforeTool",
"timestamp": "2025-12-01T10:30:00Z"
}
Tool Events (BeforeTool, AfterTool)
{
"session_id": "...",
"transcript_path": "...",
"cwd": "...",
"hook_event_name": "BeforeTool",
"timestamp": "...",
"tool_name": "write_file",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
},
"tool_response": { },
"mcp_context": {
"server_name": "github",
"tool_name": "create_issue",
"command": "...",
"args": [],
"cwd": "...",
"url": "...",
"tcp": "..."
}
}
MCP Context Transport Types:
- stdio: command, args, cwd
- SSE/HTTP: url
- WebSocket: tcp
Agent Events (BeforeAgent, AfterAgent)
{
"session_id": "...",
"hook_event_name": "BeforeAgent",
"prompt": "User's submitted text",
"prompt_response": "Final model response",
"stop_hook_active": true
}
Model Events (BeforeModel, AfterModel, BeforeToolSelection)
{
"session_id": "...",
"hook_event_name": "BeforeModel",
"llm_request": {
"model": "gemini-2.0-flash",
"messages": [
{ "role": "user", "content": "..." },
{ "role": "model", "content": "..." }
],
"config": {
"temperature": 0.7,
"maxOutputTokens": 8192,
"topP": 0.95,
"topK": 40
},
"toolConfig": {
"mode": "AUTO",
"allowedFunctionNames": ["read_file", "write_file"]
}
},
"llm_response": { }
}
Session Events
SessionStart:
{
"hook_event_name": "SessionStart",
"source": "startup"
}
Values: "startup" | "resume" | "clear"
SessionEnd:
{
"hook_event_name": "SessionEnd",
"reason": "exit"
}
Values: "exit" | "clear" | "logout" | "prompt_input_exit" | "other"
Notification Events
{
"hook_event_name": "Notification",
"notification_type": "ToolPermission",
"message": "...",
"details": { }
}
Output JSON Schema
Common Output Fields
{
"decision": "allow",
"reason": "Explanation for agent",
"systemMessage": "Message for user",
"continue": true,
"stopReason": "User-facing termination message",
"suppressOutput": false,
"hookSpecificOutput": { }
}
hookSpecificOutput by Event
SessionStart, BeforeAgent, AfterTool:
{
"hookSpecificOutput": {
"hookEventName": "AfterTool",
"additionalContext": "Extra context appended to agent"
}
}
BeforeModel:
{
"hookSpecificOutput": {
"llm_request": { },
"llm_response": { }
}
}
AfterModel:
{
"hookSpecificOutput": {
"llm_response": { }
}
}
BeforeToolSelection:
{
"hookSpecificOutput": {
"toolConfig": {
"mode": "ANY",
"allowedFunctionNames": ["read_file", "write_file"]
}
}
}
Stable LLMRequest Object
{
"model": "string",
"messages": [
{
"role": "user | model | system",
"content": "string or array of parts"
}
],
"config": {
"temperature": 0.7,
"maxOutputTokens": 8192,
"topP": 0.95,
"topK": 40
},
"toolConfig": {
"mode": "AUTO | ANY | NONE",
"allowedFunctionNames": ["tool1", "tool2"]
}
}
Stable LLMResponse Object
{
"text": "string",
"candidates": [
{
"content": {
"role": "model",
"parts": ["string"]
},
"finishReason": "STOP | MAX_TOKENS | SAFETY | RECITATION | OTHER",
"index": 0,
"safetyRatings": [
{
"category": "HARM_CATEGORY_...",
"probability": "NEGLIGIBLE | LOW | MEDIUM | HIGH",
"blocked": false
}
]
}
],
"usageMetadata": {
"promptTokenCount": 100,
"candidatesTokenCount": 200,
"totalTokenCount": 300
}
}
Note: In v1, model hooks are primarily text-focused. Non-text parts in the content array are simplified to string representation.
Exit Code Behavior
| Exit Code | Behavior | stdout Handling |
|---|---|---|
| 0 | Success | Parsed as JSON; falls back to systemMessage if invalid |
| 2 | Blocking error | stderr shown to agent/user; operation may be blocked |
| Other | Non-blocking warning | stderr logged but execution continues |
Exit Code Semantics Comparison
| Aspect | Gemini CLI | Claude Code |
|---|---|---|
| Success | 0 | 0 |
| Blocking | 2 | 2 |
| Warning | Non-zero (except 2) | Non-zero |
Source: Hooks Reference
Decision Field Values
Available Decision Values
| Value | Effect | Use Case |
|---|---|---|
| allow | Operation proceeds | Explicitly permit action |
| deny | Operation blocked | Block with reason shown to agent |
| block | Operation blocked | Similar to deny |
| ask | Prompt user | Request user confirmation |
| approve | Approve pending | Approve a previously asked operation |
Decision Examples
Deny a tool execution:
{
"decision": "deny",
"reason": "Potential secret detected in file content"
}
**Allow with