docs(hooks): comprehensive update of hook documentation and specs (#16816)

This commit is contained in:
Abhi
2026-01-21 22:13:15 -05:00
committed by GitHub
parent 982a093791
commit 0a173cac46
4 changed files with 835 additions and 2076 deletions

View File

@@ -1,4 +1,4 @@
# Hooks on Gemini CLI: Best practices
# Hooks Best Practices
This guide covers security considerations, performance optimization, debugging
techniques, and privacy considerations for developing and deploying hooks in
@@ -15,21 +15,20 @@ using parallel operations:
// Sequential operations are slower
const data1 = await fetch(url1).then((r) => r.json());
const data2 = await fetch(url2).then((r) => r.json());
const data3 = await fetch(url3).then((r) => r.json());
// Prefer parallel operations for better performance
// Start requests concurrently
const p1 = fetch(url1).then((r) => r.json());
const p2 = fetch(url2).then((r) => r.json());
const p3 = fetch(url3).then((r) => r.json());
// Wait for all results
const [data1, data2, data3] = await Promise.all([p1, p2, p3]);
const [data1, data2] = await Promise.all([p1, p2]);
```
### Cache expensive operations
Store results between invocations to avoid repeated computation:
Store results between invocations to avoid repeated computation, especially for
hooks that run frequently (like `BeforeTool` or `AfterModel`).
```javascript
const fs = require('fs');
@@ -54,6 +53,7 @@ async function main() {
const cacheKey = `tool-list-${(Date.now() / 3600000) | 0}`; // Hourly cache
if (cache[cacheKey]) {
// Write JSON to stdout
console.log(JSON.stringify(cache[cacheKey]));
return;
}
@@ -70,32 +70,20 @@ async function main() {
### Use appropriate events
Choose hook events that match your use case to avoid unnecessary execution.
`AfterAgent` fires once per agent loop completion, while `AfterModel` fires
after every LLM call (potentially multiple times per loop):
```json
// If checking final completion, use AfterAgent instead of AfterModel
{
"hooks": {
"AfterAgent": [
{
"matcher": "*",
"hooks": [
{
"name": "final-checker",
"command": "./check-completion.sh"
}
]
}
]
}
}
```
- **`AfterAgent`**: Fires **once** per turn after the model finishes its final
response. Use this for quality validation (Retries) or final logging.
- **`AfterModel`**: Fires after **every chunk** of LLM output. Use this for
real-time redaction, PII filtering, or monitoring output as it streams.
If you only need to check the final completion, use `AfterAgent` to save
performance.
### Filter with matchers
Use specific matchers to avoid unnecessary hook execution. Instead of matching
all tools with `*`, specify only the tools you need:
all tools with `*`, specify only the tools you need. This saves the overhead of
spawning a process for irrelevant events.
```json
{
@@ -111,30 +99,32 @@ all tools with `*`, specify only the tools you need:
### Optimize JSON parsing
For large inputs, use streaming JSON parsers to avoid loading everything into
memory:
```javascript
// Standard approach: parse entire input
const input = JSON.parse(await readStdin());
const content = input.tool_input.content;
// For very large inputs: stream and extract only needed fields
const { createReadStream } = require('fs');
const JSONStream = require('JSONStream');
const stream = createReadStream(0).pipe(JSONStream.parse('tool_input.content'));
let content = '';
stream.on('data', (chunk) => {
content += chunk;
});
```
For large inputs (like `AfterModel` receiving a large context), standard JSON
parsing can be slow. If you only need one field, consider streaming parsers or
lightweight extraction logic, though for most shell scripts `jq` is sufficient.
## Debugging
### The "Strict JSON" rule
The most common cause of hook failure is "polluting" the standard output.
- **stdout** is for **JSON only**.
- **stderr** is for **logs and text**.
**Good:**
```bash
#!/bin/bash
echo "Starting check..." >&2 # <--- Redirect to stderr
echo '{"decision": "allow"}'
```
### Log to files
Write debug information to dedicated log files:
Since hooks run in the background, writing to a dedicated log file is often the
easiest way to debug complex logic.
```bash
#!/usr/bin/env bash
@@ -151,6 +141,9 @@ log "Received input: ${input:0:100}..."
# Hook logic here
log "Hook completed successfully"
# Always output valid JSON to stdout at the end, even if just empty
echo "{}"
```
### Use stderr for errors
@@ -162,6 +155,7 @@ try {
const result = dangerousOperation();
console.log(JSON.stringify({ result }));
} catch (error) {
// Write the error description to stderr so the user/agent sees it
console.error(`Hook error: ${error.message}`);
process.exit(2); // Blocking error
}
@@ -169,7 +163,8 @@ try {
### Test hooks independently
Run hook scripts manually with sample JSON input:
Run hook scripts manually with sample JSON input to verify they behave as
expected before hooking them up to the CLI.
```bash
# Create test input
@@ -191,33 +186,46 @@ cat test-input.json | .gemini/hooks/my-hook.sh
# Check exit code
echo "Exit code: $?"
```
### Check exit codes
Ensure your script returns the correct exit code:
Gemini CLI uses exit codes for high-level flow control:
- **Exit 0 (Success)**: The hook ran successfully. The CLI parses `stdout` for
JSON decisions.
- **Exit 2 (System Block)**: A critical block occurred. `stderr` is used as the
reason.
- For **Agent/Model** events, this aborts the turn.
- For **Tool** events, this blocks the tool but allows the agent to continue.
- For **AfterAgent**, this triggers an automatic retry turn.
> **TIP**
>
> **Blocking vs. Stopping**: Use `decision: "deny"` (or Exit Code 2) to block a
> **specific action**. Use `{"continue": false}` in your JSON output to **kill
> the entire agent loop** immediately.
```bash
#!/usr/bin/env bash
set -e # Exit on error
set -e
# Hook logic
process_input() {
# ...
}
if process_input; then
echo "Success message"
echo '{"decision": "allow"}'
exit 0
else
echo "Error message" >&2
echo "Critical validation failure" >&2
exit 2
fi
```
### Enable telemetry
Hook execution is logged when `telemetry.logPrompts` is enabled:
Hook execution is logged when `telemetry.logPrompts` is enabled. You can view
these logs to debug execution flow.
```json
{
@@ -227,11 +235,10 @@ Hook execution is logged when `telemetry.logPrompts` is enabled:
}
```
View hook telemetry in logs to debug execution issues.
### Use hook panel
The `/hooks panel` command shows execution status and recent output:
The `/hooks panel` command inside the CLI shows execution status and recent
output:
```bash
/hooks panel
@@ -255,18 +262,64 @@ Begin with basic logging hooks before implementing complex logic:
# Simple logging hook to understand input structure
input=$(cat)
echo "$input" >> .gemini/hook-inputs.log
echo "Logged input"
# Always return valid JSON
echo "{}"
```
### Documenting your hooks
Maintainability is critical for complex hook systems. Use descriptions and
comments to help yourself and others understand why a hook exists.
**Use the `description` field**: This text is displayed in the `/hooks panel` UI
and helps diagnose issues.
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file|replace",
"hooks": [
{
"name": "secret-scanner",
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh",
"description": "Scans code changes for API keys and secrets before writing"
}
]
}
]
}
}
```
**Add comments in hook scripts**: Explain performance expectations and
dependencies.
```javascript
#!/usr/bin/env node
/**
* RAG Tool Filter Hook
*
* Reduces the tool space by extracting keywords from the user's request.
*
* Performance: ~500ms average
* Dependencies: @google/generative-ai
*/
```
### Use JSON libraries
Parse JSON with proper libraries instead of text processing:
Parse JSON with proper libraries instead of text processing.
**Bad:**
```bash
# Fragile text parsing
tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')
```
**Good:**
@@ -274,6 +327,7 @@ tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')
```bash
# Robust JSON parsing
tool_name=$(echo "$input" | jq -r '.tool_name')
```
### Make scripts executable
@@ -283,6 +337,7 @@ Always make hook scripts executable:
```bash
chmod +x .gemini/hooks/*.sh
chmod +x .gemini/hooks/*.js
```
### Version control
@@ -292,7 +347,7 @@ Commit hooks to share with your team:
```bash
git add .gemini/hooks/
git add .gemini/settings.json
git commit -m "Add project hooks for security and testing"
```
**`.gitignore` considerations:**
@@ -306,295 +361,10 @@ git commit -m "Add project hooks for security and testing"
# Keep hook scripts
!.gemini/hooks/*.sh
!.gemini/hooks/*.js
```
### Document behavior
Add descriptions to help others understand your hooks:
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file|replace",
"hooks": [
{
"name": "secret-scanner",
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh",
"description": "Scans code changes for API keys, passwords, and other secrets before writing"
}
]
}
]
}
}
```
Add comments in hook scripts:
```javascript
#!/usr/bin/env node
/**
* RAG Tool Filter Hook
*
* This hook reduces the tool space from 100+ tools to ~15 relevant ones
* by extracting keywords from the user's request and filtering tools
* based on semantic similarity.
*
* Performance: ~500ms average, cached tool embeddings
* Dependencies: @google/generative-ai
*/
```
## Troubleshooting
### Hook not executing
**Check hook name in `/hooks panel`:**
```bash
/hooks panel
```
Verify the hook appears in the list and is enabled.
**Verify matcher pattern:**
```bash
# Test regex pattern
echo "write_file|replace" | grep -E "write_.*|replace"
```
**Check disabled list:**
```json
{
"hooks": {
"disabled": ["my-hook-name"]
}
}
```
**Ensure script is executable:**
```bash
ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh
```
**Verify script path:**
```bash
# Check path expansion
echo "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# Verify file exists
test -f "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh" && echo "File exists"
```
### Hook timing out
**Check configured timeout:**
```json
{
"name": "slow-hook",
"timeout": 60000
}
```
**Optimize slow operations:**
```javascript
// Before: Sequential operations (slow)
for (const item of items) {
await processItem(item);
}
// After: Parallel operations (fast)
await Promise.all(items.map((item) => processItem(item)));
```
**Use caching:**
```javascript
const cache = new Map();
async function getCachedData(key) {
if (cache.has(key)) {
return cache.get(key);
}
const data = await fetchData(key);
cache.set(key, data);
return data;
}
```
**Consider splitting into multiple faster hooks:**
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file",
"hooks": [
{
"name": "quick-check",
"command": "./quick-validation.sh",
"timeout": 1000
}
]
},
{
"matcher": "write_file",
"hooks": [
{
"name": "deep-check",
"command": "./deep-analysis.sh",
"timeout": 30000
}
]
}
]
}
}
```
### Invalid JSON output
**Validate JSON before outputting:**
```bash
#!/usr/bin/env bash
output='{"decision": "allow"}'
# Validate JSON
if echo "$output" | jq empty 2>/dev/null; then
echo "$output"
else
echo "Invalid JSON generated" >&2
exit 1
fi
```
**Ensure proper quoting and escaping:**
```javascript
// Bad: Unescaped string interpolation
const message = `User said: ${userInput}`;
console.log(JSON.stringify({ message }));
// Good: Automatic escaping
console.log(JSON.stringify({ message: `User said: ${userInput}` }));
```
**Check for binary data or control characters:**
```javascript
function sanitizeForJSON(str) {
return str.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); // Remove control chars
}
const cleanContent = sanitizeForJSON(content);
console.log(JSON.stringify({ content: cleanContent }));
```
### Exit code issues
**Verify script returns correct codes:**
```bash
#!/usr/bin/env bash
set -e # Exit on error
# Processing logic
if validate_input; then
echo "Success"
exit 0
else
echo "Validation failed" >&2
exit 2
fi
```
**Check for unintended errors:**
```bash
#!/usr/bin/env bash
# Don't use 'set -e' if you want to handle errors explicitly
# set -e
if ! command_that_might_fail; then
# Handle error
echo "Command failed but continuing" >&2
fi
# Always exit explicitly
exit 0
```
**Use trap for cleanup:**
```bash
#!/usr/bin/env bash
cleanup() {
# Cleanup logic
rm -f /tmp/hook-temp-*
}
trap cleanup EXIT
# Hook logic here
```
### Environment variables not available
**Check if variable is set:**
```bash
#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then
echo "GEMINI_PROJECT_DIR not set" >&2
exit 1
fi
if [ -z "$CUSTOM_VAR" ]; then
echo "Warning: CUSTOM_VAR not set, using default" >&2
CUSTOM_VAR="default-value"
fi
```
**Debug available variables:**
```bash
#!/usr/bin/env bash
# List all environment variables
env > .gemini/hook-env.log
# Check specific variables
echo "GEMINI_PROJECT_DIR: $GEMINI_PROJECT_DIR" >> .gemini/hook-env.log
echo "GEMINI_SESSION_ID: $GEMINI_SESSION_ID" >> .gemini/hook-env.log
echo "GEMINI_API_KEY: ${GEMINI_API_KEY:+<set>}" >> .gemini/hook-env.log
```
**Use .env files:**
```bash
#!/usr/bin/env bash
# Load .env file if it exists
if [ -f "$GEMINI_PROJECT_DIR/.env" ]; then
source "$GEMINI_PROJECT_DIR/.env"
fi
```
## Using Hooks Securely
## Hook security
### Threat Model
@@ -621,11 +391,10 @@ When you open a project with hooks defined in `.gemini/settings.json`:
it).
5. **Trust**: The hook is marked as "trusted" for this project.
> [!IMPORTANT] **Modification Detection**: If the `command` string of a project
> hook is changed (e.g., by a `git pull`), its identity changes. Gemini CLI will
> treat it as a **new, untrusted hook** and warn you again. This prevents
> malicious actors from silently swapping a verified command for a malicious
> one.
> **Modification detection**: If the `command` string of a project hook is
> changed (e.g., by a `git pull`), its identity changes. Gemini CLI will treat
> it as a **new, untrusted hook** and warn you again. This prevents malicious
> actors from silently swapping a verified command for a malicious one.
### Risks
@@ -646,32 +415,134 @@ When you open a project with hooks defined in `.gemini/settings.json`:
publishers, well-known community members).
- Be cautious with obfuscated scripts or compiled binaries from unknown sources.
#### Sanitize Environment
#### Sanitize environment
Hooks inherit the environment of the Gemini CLI process, which may include
sensitive API keys. Gemini CLI attempts to sanitize sensitive variables, but you
should be cautious.
sensitive API keys. Gemini CLI provides a
[redaction system](/docs/get-started/configuration#environment-variable-redaction)
that automatically filters variables matching sensitive patterns (e.g., `KEY`,
`TOKEN`).
- **Avoid printing environment variables** to stdout/stderr unless necessary.
- **Use `.env` files** to securely manage sensitive variables, ensuring they are
excluded from version control.
> **Disabled by Default**: Environment redaction is currently **OFF by
> default**. We strongly recommend enabling it if you are running third-party
> hooks or working in sensitive environments.
**System Administrators:** You can enforce environment variable redaction by
default in the system configuration (e.g., `/etc/gemini-cli/settings.json`):
**Impact on hooks:**
- **Security**: Prevents your hook scripts from accidentally leaking secrets.
- **Troubleshooting**: If your hook depends on a specific environment variable
that is being blocked, you must explicitly allow it in `settings.json`.
```json
{
"security": {
"environmentVariableRedaction": {
"enabled": true,
"blocked": ["MY_SECRET_KEY"],
"allowed": ["SAFE_VAR"]
"allowed": ["MY_REQUIRED_TOOL_KEY"]
}
}
}
```
## Authoring Secure Hooks
**System administrators:** You can enforce redaction for all users in the system
configuration.
## Troubleshooting
### Hook not executing
**Check hook name in `/hooks panel`:** Verify the hook appears in the list and
is enabled.
**Verify matcher pattern:**
```bash
# Test regex pattern
echo "write_file|replace" | grep -E "write_.*|replace"
```
**Check disabled list:** Verify the hook is not listed in your `settings.json`:
```json
{
"hooks": {
"disabled": ["my-hook-name"]
}
}
```
**Ensure script is executable**: For macOS and Linux users, verify the script
has execution permissions:
```bash
ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh
```
**Verify script path:** Ensure the path in `settings.json` resolves correctly.
```bash
# Check path expansion
echo "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# Verify file exists
test -f "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh" && echo "File exists"
```
### Hook timing out
**Check configured timeout:** The default is 60000ms (1 minute). You can
increase this in `settings.json`:
```json
{
"name": "slow-hook",
"timeout": 120000
}
```
**Optimize slow operations:** Move heavy processing to background tasks or use
caching.
### Invalid JSON output
**Validate JSON before outputting:**
```bash
#!/usr/bin/env bash
output='{"decision": "allow"}'
# Validate JSON
if echo "$output" | jq empty 2>/dev/null; then
echo "$output"
else
echo "Invalid JSON generated" >&2
exit 1
fi
```
### Environment variables not available
**Check if variable is set:**
```bash
#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then
echo "GEMINI_PROJECT_DIR not set" >&2
exit 1
fi
```
**Debug available variables:**
```bash
env > .gemini/hook-env.log
```
## Authoring secure hooks
When writing your own hooks, follow these practices to ensure they are robust
and secure.
@@ -766,40 +637,17 @@ function containsSecret(content) {
## Privacy considerations
Hook inputs and outputs may contain sensitive information. Gemini CLI respects
the `telemetry.logPrompts` setting for hook data logging.
Hook inputs and outputs may contain sensitive information.
### What data is collected
Hook telemetry may include:
- **Hook inputs:** User prompts, tool arguments, file contents
- **Hook outputs:** Hook responses, decision reasons, added context
- **Standard streams:** stdout and stderr from hook processes
- **Execution metadata:** Hook name, event type, duration, success/failure
Hook telemetry may include inputs (prompts, code) and outputs (decisions,
reasons) unless disabled.
### Privacy settings
**Enabled (default):**
Full hook I/O is logged to telemetry. Use this when:
- Developing and debugging hooks
- Telemetry is redirected to a trusted enterprise system
- You understand and accept the privacy implications
**Disabled:**
Only metadata is logged (event name, duration, success/failure). Hook inputs and
outputs are excluded. Use this when:
- Sending telemetry to third-party systems
- Working with sensitive data
- Privacy regulations require minimizing data collection
### Configuration
**Disable PII logging in settings:**
**Disable PII logging:** If you are working with sensitive data, disable prompt
logging in your settings:
```json
{
@@ -809,48 +657,19 @@ outputs are excluded. Use this when:
}
```
**Disable via environment variable:**
**Suppress Output:** Individual hooks can request their metadata be hidden from
logs and telemetry by returning `"suppressOutput": true` in their JSON response.
```bash
export GEMINI_TELEMETRY_LOG_PROMPTS=false
```
> **Note**
> `suppressOutput` only affects background logging. Any `systemMessage` or
> `reason` included in the JSON will still be displayed to the user in the
> terminal.
### Sensitive data in hooks
If your hooks process sensitive data:
1. **Minimize logging:** Don't write sensitive data to log files
2. **Sanitize outputs:** Remove sensitive data before outputting
3. **Use secure storage:** Encrypt sensitive data at rest
4. **Limit access:** Restrict hook script permissions
**Example sanitization:**
```javascript
function sanitizeOutput(data) {
const sanitized = { ...data };
// Remove sensitive fields
delete sanitized.apiKey;
delete sanitized.password;
// Redact sensitive strings
if (sanitized.content) {
sanitized.content = sanitized.content.replace(
/api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/gi,
'[REDACTED]',
);
}
return sanitized;
}
console.log(JSON.stringify(sanitizeOutput(hookOutput)));
```
## Learn more
- [Hooks Reference](index.md) - Complete API reference
- [Writing Hooks](writing-hooks.md) - Tutorial and examples
- [Configuration](../get-started/configuration.md) - Gemini CLI settings
- [Hooks Design Document](../hooks-design.md) - Technical architecture
1. **Minimize logging:** Don't write sensitive data to log files.
2. **Sanitize outputs:** Remove sensitive data before outputting JSON or writing
to stderr.

View File

@@ -4,109 +4,116 @@ Hooks are scripts or programs that Gemini CLI executes at specific points in the
agentic loop, allowing you to intercept and customize behavior without modifying
the CLI's source code.
> [!NOTE] **Hooks are currently an experimental feature.**
>
> To use hooks, you must explicitly enable them in your `settings.json`:
>
> ```json
> {
> "tools": { "enableHooks": true },
> "hooks": { "enabled": true }
> }
> ```
>
> Both of these are needed in this experimental phase.
## Availability
See [writing hooks guide](writing-hooks.md) for a tutorial on creating your
first hook and a comprehensive example.
> **Experimental Feature**: Hooks are currently enabled by default only in the
> **Preview** and **Nightly** release channels.
See [hooks reference](reference.md) for the technical specification of the I/O
schemas.
If you are on the Stable channel, you must explicitly enable the hooks system in
your `settings.json`:
See [best practices](best-practices.md) for guidelines on security, performance,
and debugging.
```json
{
"hooksConfig": {
"enabled": true
}
}
```
- **[Writing hooks guide](/docs/hooks/writing-hooks)**: A tutorial on creating
your first hook with comprehensive examples.
- **[Hooks reference](/docs/hooks/reference)**: The definitive technical
specification of I/O schemas and exit codes.
- **[Best practices](/docs/hooks/best-practices)**: Guidelines on security,
performance, and debugging.
## What are hooks?
With hooks, you can:
- **Add context:** Inject relevant information before the model processes a
request
- **Validate actions:** Review and block potentially dangerous operations
- **Enforce policies:** Implement security and compliance requirements
- **Log interactions:** Track tool usage and model responses
- **Optimize behavior:** Dynamically adjust tool selection or model parameters
Hooks run synchronously as part of the agent loop—when a hook event fires,
Gemini CLI waits for all matching hooks to complete before continuing.
## Security and Risks
With hooks, you can:
> **Warning: Hooks execute arbitrary code with your user privileges.**
>
> By configuring hooks, you are explicitly allowing Gemini CLI to run shell
> commands on your machine. Malicious or poorly configured hooks can:
- **Exfiltrate data**: Read sensitive files (`.env`, ssh keys) and send them to
remote servers.
- **Modify system**: Delete files, install malware, or change system settings.
- **Consume resources**: Run infinite loops or crash your system.
**Project-level hooks** (in `.gemini/settings.json`) and **Extension hooks** are
particularly risky when opening third-party projects or extensions from
untrusted authors. Gemini CLI will **warn you** the first time it detects a new
project hook (identified by its name and command), but it is **your
responsibility** to review these hooks (and any installed extensions) before
trusting them.
> **Note:** Extension hooks are subject to a mandatory security warning and
> consent flow during extension installation or update if hooks are detected.
> You must explicitly approve the installation or update of any extension that
> contains hooks.
See [Security Considerations](best-practices.md#using-hooks-securely) for a
detailed threat model and mitigation strategies.
- **Add context:** Inject relevant information (like git history) before the
model processes a request.
- **Validate actions:** Review tool arguments and block potentially dangerous
operations.
- **Enforce policies:** Implement security scanners and compliance checks.
- **Log interactions:** Track tool usage and model responses for auditing.
- **Optimize behavior:** Dynamically filter available tools or adjust model
parameters.
## Core concepts
### Hook events
Hooks are triggered by specific events in Gemini CLI's lifecycle. The following
table lists all available hook events:
Hooks are triggered by specific events in Gemini CLI's lifecycle.
| Event | When It Fires | Common Use Cases |
| --------------------- | --------------------------------------------- | ------------------------------------------ |
| `SessionStart` | When a session begins | Initialize resources, load context |
| `SessionEnd` | When a session ends | Clean up, save state |
| `BeforeAgent` | After user submits prompt, before planning | Add context, validate prompts |
| `AfterAgent` | When agent loop ends | Review output, force continuation |
| `BeforeModel` | Before sending request to LLM | Modify prompts, add instructions |
| `AfterModel` | After receiving LLM response | Filter responses, log interactions |
| `BeforeToolSelection` | Before LLM selects tools (after BeforeModel) | Filter available tools, optimize selection |
| `BeforeTool` | Before a tool executes | Validate arguments, block dangerous ops |
| `AfterTool` | After a tool executes | Process results, run tests |
| `PreCompress` | Before context compression | Save state, notify user |
| `Notification` | When a notification occurs (e.g., permission) | Auto-approve, log decisions |
| Event | When It Fires | Impact | Common Use Cases |
| --------------------- | ---------------------------------------------- | ---------------------- | -------------------------------------------- |
| `SessionStart` | When a session begins (startup, resume, clear) | Inject Context | Initialize resources, load context |
| `SessionEnd` | When a session ends (exit, clear) | Advisory | Clean up, save state |
| `BeforeAgent` | After user submits prompt, before planning | Block Turn / Context | Add context, validate prompts, block turns |
| `AfterAgent` | When agent loop ends | Retry / Halt | Review output, force retry or halt execution |
| `BeforeModel` | Before sending request to LLM | Block Turn / Mock | Modify prompts, swap models, mock responses |
| `AfterModel` | After receiving LLM response | Block Turn / Redact | Filter/redact responses, log interactions |
| `BeforeToolSelection` | Before LLM selects tools | Filter Tools | Filter available tools, optimize selection |
| `BeforeTool` | Before a tool executes | Block Tool / Rewrite | Validate arguments, block dangerous ops |
| `AfterTool` | After a tool executes | Block Result / Context | Process results, run tests, hide results |
| `PreCompress` | Before context compression | Advisory | Save state, notify user |
| `Notification` | When a system notification occurs | Advisory | Forward to desktop alerts, logging |
### Hook types
### Global mechanics
Gemini CLI currently supports **command hooks** that run shell commands or
scripts:
Understanding these core principles is essential for building robust hooks.
```json
{
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh",
"timeout": 30000
}
```
#### Strict JSON requirements (The "Golden Rule")
**Note:** Plugin hooks (npm packages) are planned for a future release.
Hooks communicate via `stdin` (Input) and `stdout` (Output).
### Matchers
1. **Silence is Mandatory**: Your script **must not** print any plain text to
`stdout` other than the final JSON object. **Even a single `echo` or `print`
call before the JSON will break parsing.**
2. **Pollution = Failure**: If `stdout` contains non-JSON text, parsing will
fail. The CLI will default to "Allow" and treat the entire output as a
`systemMessage`.
3. **Debug via Stderr**: Use `stderr` for **all** logging and debugging (e.g.,
`echo "debug" >&2`). Gemini CLI captures `stderr` but never attempts to parse
it as JSON.
For tool-related events (`BeforeTool`, `AfterTool`), you can filter which tools
trigger the hook:
#### Exit codes
Gemini CLI uses exit codes to determine the high-level outcome of a hook
execution:
| Exit Code | Label | Behavioral Impact |
| --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **0** | **Success** | The `stdout` is parsed as JSON. **Preferred code** for all logic, including intentional blocks (e.g., `{"decision": "deny"}`). |
| **2** | **System Block** | **Critical Block**. The target action (tool, turn, or stop) is aborted. `stderr` is used as the rejection reason. High severity; used for security stops or script failures. |
| **Other** | **Warning** | Non-fatal failure. A warning is shown, but the interaction proceeds using original parameters. |
#### Matchers
You can filter which specific tools or triggers fire your hook using the
`matcher` field.
- **Tool events** (`BeforeTool`, `AfterTool`): Matchers are **Regular
Expressions**. (e.g., `"write_.*"`).
- **Lifecycle events**: Matchers are **Exact Strings**. (e.g., `"startup"`).
- **Wildcards**: `"*"` or `""` (empty string) matches all occurrences.
## Configuration
Hook definitions are configured in `settings.json`. Gemini CLI merges
configurations from multiple layers in the following order of precedence
(highest to lowest):
1. **Project settings**: `.gemini/settings.json` in the current directory.
2. **User settings**: `~/.gemini/settings.json`.
3. **System settings**: `/etc/gemini-cli/settings.json`.
4. **Extensions**: Hooks defined by installed extensions.
### Configuration schema
```json
{
@@ -114,382 +121,13 @@ trigger the hook:
"BeforeTool": [
{
"matcher": "write_file|replace",
"hooks": [
/* hooks for write operations */
]
}
]
}
}
```
**Matcher patterns:**
- **Exact match:** `"read_file"` matches only `read_file`
- **Regex:** `"write_.*|replace"` matches `write_file`, `replace`
- **Wildcard:** `"*"` or `""` matches all tools
**Session event matchers:**
- **SessionStart:** `startup`, `resume`, `clear`
- **SessionEnd:** `exit`, `clear`, `logout`, `prompt_input_exit`
- **PreCompress:** `manual`, `auto`
- **Notification:** `ToolPermission`
## Hook input/output contract
### Command hook communication
Hooks communicate via:
- **Input:** JSON on stdin
- **Output:** Exit code + stdout/stderr
### Exit codes
- **0:** Success - stdout shown to user (or injected as context for some events)
- **2:** Blocking error - stderr shown to agent/user, operation may be blocked
- **Other:** Non-blocking warning - logged but execution continues
### Common input fields
Every hook receives these base fields:
```json
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/path/to/project",
"hook_event_name": "BeforeTool",
"timestamp": "2025-12-01T10:30:00Z"
// ... event-specific fields
}
```
### Event-specific fields
#### BeforeTool
**Input:**
```json
{
"tool_name": "write_file",
"tool_input": {
"file_path": "/path/to/file.ts",
"content": "..."
}
}
```
**Output (JSON on stdout):**
```json
{
"decision": "allow|deny|ask|block",
"reason": "Explanation shown to agent",
"systemMessage": "Message shown to user"
}
```
Or simple exit codes:
- Exit 0 = allow (stdout shown to user)
- Exit 2 = deny (stderr shown to agent)
#### AfterTool
**Input:**
```json
{
"tool_name": "read_file",
"tool_input": { "file_path": "..." },
"tool_response": "file contents..."
}
```
**Output:**
```json
{
"decision": "allow|deny",
"hookSpecificOutput": {
"hookEventName": "AfterTool",
"additionalContext": "Extra context for agent"
}
}
```
#### BeforeAgent
**Input:**
```json
{
"prompt": "Fix the authentication bug"
}
```
**Output:**
```json
{
"decision": "allow|deny",
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "Recent project decisions: ..."
}
}
```
#### BeforeModel
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [{ "role": "user", "content": "Hello" }],
"config": { "temperature": 0.7 },
"toolConfig": {
"functionCallingConfig": {
"mode": "AUTO",
"allowedFunctionNames": ["read_file", "write_file"]
}
}
}
}
```
**Output:**
```json
{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeModel",
"llm_request": {
"messages": [
{ "role": "system", "content": "Additional instructions..." },
{ "role": "user", "content": "Hello" }
]
}
}
}
```
#### AfterModel
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [
/* ... */
],
"config": {
/* ... */
},
"toolConfig": {
/* ... */
}
},
"llm_response": {
"text": "string",
"candidates": [
{
"content": {
"role": "model",
"parts": ["array of content parts"]
},
"finishReason": "STOP"
}
]
}
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "AfterModel",
"llm_response": {
"candidate": {
/* modified response */
}
}
}
}
```
#### BeforeToolSelection
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [
/* ... */
],
"toolConfig": {
"functionCallingConfig": {
"mode": "AUTO",
"allowedFunctionNames": [
/* 100+ tools */
]
}
}
}
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "BeforeToolSelection",
"toolConfig": {
"functionCallingConfig": {
"mode": "ANY",
"allowedFunctionNames": ["read_file", "write_file", "replace"]
}
}
}
}
```
Or simple output (comma-separated tool names sets mode to ANY):
```bash
echo "read_file,write_file,replace"
```
#### SessionStart
**Input:**
```json
{
"source": "startup|resume|clear"
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Loaded 5 project memories"
}
}
```
#### SessionEnd
**Input:**
```json
{
"reason": "exit|clear|logout|prompt_input_exit|other"
}
```
No structured output expected (but stdout/stderr logged).
#### PreCompress
**Input:**
```json
{
"trigger": "manual|auto"
}
```
**Output:**
```json
{
"systemMessage": "Compression starting..."
}
```
#### Notification
**Input:**
```json
{
"notification_type": "ToolPermission",
"message": "string",
"details": {
/* notification details */
}
}
```
**Output:**
```json
{
"systemMessage": "Notification logged"
}
```
## Configuration
Hook definitions are configured in `settings.json` files using the `hooks`
object. Configuration can be specified at multiple levels with defined
precedence rules.
### Configuration layers
Hook configurations are applied in the following order of execution (lower
numbers run first):
1. **Project settings:** `.gemini/settings.json` in your project directory
(highest priority)
2. **User settings:** `~/.gemini/settings.json`
3. **System settings:** `/etc/gemini-cli/settings.json`
4. **Extensions:** Internal hooks defined by installed extensions (lowest
priority). See [Extensions documentation](../extensions/index.md#hooks) for
details on how extensions define and configure hooks.
#### Deduplication and shadowing
If multiple hooks with the identical **name** and **command** are discovered
across different configuration layers, Gemini CLI deduplicates them. The hook
from the higher-priority layer (e.g., Project) will be kept, and others will be
ignored.
Within each level, hooks run in the order they are declared in the
configuration.
### Configuration schema
```json
{
"hooks": {
"EventName": [
{
"matcher": "pattern",
"hooks": [
{
"name": "hook-identifier",
"name": "security-check",
"type": "command",
"command": "./path/to/script.sh",
"description": "What this hook does",
"timeout": 30000
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/security.sh",
"timeout": 5000,
"sequential": false
}
]
}
@@ -498,226 +136,33 @@ configuration.
}
```
**Configuration properties:**
- **`name`** (string, recommended): Unique identifier for the hook used in
`/hooks enable/disable` commands. If omitted, the `command` path is used as
the identifier.
- **`type`** (string, required): Hook type - currently only `"command"` is
supported
- **`command`** (string, required): Path to the script or command to execute
- **`description`** (string, optional): Human-readable description shown in
`/hooks panel`
- **`timeout`** (number, optional): Timeout in milliseconds (default: 60000)
- **`matcher`** (string, optional): Pattern to filter when hook runs (event
matchers only)
### Environment variables
Hooks have access to:
Hooks are executed with a sanitized environment.
- `GEMINI_PROJECT_DIR`: Project root directory
- `GEMINI_SESSION_ID`: Current session ID
- `GEMINI_API_KEY`: Gemini API key (if configured)
- All other environment variables from the parent process
- `GEMINI_PROJECT_DIR`: The absolute path to the project root.
- `GEMINI_SESSION_ID`: The unique ID for the current session.
- `GEMINI_CWD`: The current working directory.
- `CLAUDE_PROJECT_DIR`: (Alias) Provided for compatibility.
## Security and risks
> **Warning: Hooks execute arbitrary code with your user privileges.** By
> configuring hooks, you are allowing scripts to run shell commands on your
> machine.
**Project-level hooks** are particularly risky when opening untrusted projects.
Gemini CLI **fingerprints** project hooks. If a hook's name or command changes
(e.g., via `git pull`), it is treated as a **new, untrusted hook** and you will
be warned before it executes.
See [Security Considerations](/docs/hooks/best-practices#using-hooks-securely)
for a detailed threat model.
## Managing hooks
### View registered hooks
Use the CLI commands to manage hooks without editing JSON manually:
Use the `/hooks panel` command to view all registered hooks:
```bash
/hooks panel
```
This command displays:
- All configured hooks organized by event
- Hook source (user, project, system)
- Hook type (command or plugin)
- Individual hook status (enabled/disabled)
### Enable and disable all hooks at once
You can enable or disable all hooks at once using commands:
```bash
/hooks enable-all
/hooks disable-all
```
These commands provide a shortcut to enable or disable all configured hooks
without managing them individually. The `enable-all` command removes all hooks
from the `hooks.disabled` array, while `disable-all` adds all configured hooks
to the disabled list. Changes take effect immediately without requiring a
restart.
### Enable and disable individual hooks
You can enable or disable individual hooks using commands:
```bash
/hooks enable hook-name
/hooks disable hook-name
```
These commands allow you to control hook execution without editing configuration
files. The hook name should match the `name` field in your hook configuration.
Changes made via these commands are persisted to your settings. The settings are
saved to workspace scope if available, otherwise to your global user settings
(`~/.gemini/settings.json`).
### Disabled hooks configuration
To permanently disable hooks, add them to the `hooks.disabled` array in your
`settings.json`:
```json
{
"hooks": {
"disabled": ["secret-scanner", "auto-test"]
}
}
```
**Note:** The `hooks.disabled` array uses a UNION merge strategy. Disabled hooks
from all configuration levels (user, project, system) are combined and
deduplicated, meaning a hook disabled at any level remains disabled.
## Migration from Claude Code
If you have hooks configured for Claude Code, you can migrate them:
```bash
gemini hooks migrate --from-claude
```
This command:
- Reads `.claude/settings.json`
- Converts event names (`PreToolUse``BeforeTool`, etc.)
- Translates tool names (`Bash``run_shell_command`, `replace``replace`)
- Updates matcher patterns
- Writes to `.gemini/settings.json`
### Event name mapping
| Claude Code | Gemini CLI |
| ------------------ | -------------- |
| `PreToolUse` | `BeforeTool` |
| `PostToolUse` | `AfterTool` |
| `UserPromptSubmit` | `BeforeAgent` |
| `Stop` | `AfterAgent` |
| `Notification` | `Notification` |
| `SessionStart` | `SessionStart` |
| `SessionEnd` | `SessionEnd` |
| `PreCompact` | `PreCompress` |
### Tool name mapping
| Claude Code | Gemini CLI |
| ----------- | --------------------- |
| `Bash` | `run_shell_command` |
| `Edit` | `replace` |
| `Read` | `read_file` |
| `Write` | `write_file` |
| `Glob` | `glob` |
| `Grep` | `search_file_content` |
| `LS` | `list_directory` |
## Tool and Event Matchers Reference
### Available tool names for matchers
The following built-in tools can be used in `BeforeTool` and `AfterTool` hook
matchers:
#### File operations
- `read_file` - Read a single file
- `read_many_files` - Read multiple files at once
- `write_file` - Create or overwrite a file
- `replace` - Edit file content with find/replace
#### File system
- `list_directory` - List directory contents
- `glob` - Find files matching a pattern
- `search_file_content` - Search within file contents
#### Execution
- `run_shell_command` - Execute shell commands
#### Web and external
- `google_web_search` - Google Search with grounding
- `web_fetch` - Fetch web page content
#### Agent features
- `write_todos` - Manage TODO items
- `save_memory` - Save information to memory
- `delegate_to_agent` - Delegate tasks to sub-agents
#### Example matchers
```json
{
"matcher": "write_file|replace" // File editing tools
}
```
```json
{
"matcher": "read_.*" // All read operations
}
```
```json
{
"matcher": "run_shell_command" // Only shell commands
}
```
```json
{
"matcher": "*" // All tools
}
```
### Event-specific matchers
#### SessionStart event matchers
- `startup` - Fresh session start
- `resume` - Resuming a previous session
- `clear` - Session cleared
#### SessionEnd event matchers
- `exit` - Normal exit
- `clear` - Session cleared
- `logout` - User logged out
- `prompt_input_exit` - Exit from prompt input
- `other` - Other reasons
#### PreCompress event matchers
- `manual` - Manually triggered compression
- `auto` - Automatically triggered compression
#### Notification event matchers
- `ToolPermission` - Tool permission notifications
## Learn more
- [Writing Hooks](writing-hooks.md) - Tutorial and comprehensive example
- [Best Practices](best-practices.md) - Security, performance, and debugging
- [Custom Commands](../cli/custom-commands.md) - Create reusable prompt
shortcuts
- [Configuration](../get-started/configuration.md) - Gemini CLI configuration
options
- [Hooks Design Document](../hooks-design.md) - Technical architecture details
- **View hooks:** `/hooks panel`
- **Enable/Disable all:** `/hooks enable-all` or `/hooks disable-all`
- **Toggle individual:** `/hooks enable <name>` or `/hooks disable <name>`

View File

@@ -1,178 +1,295 @@
# Hooks Reference
# Hooks reference
This document provides the technical specification for Gemini CLI hooks,
including the JSON schemas for input and output, exit code behaviors, and the
stable model API.
including JSON schemas and API details.
## Communication Protocol
## Global hook mechanics
Hooks communicate with Gemini CLI via standard streams and exit codes:
- **Input**: Gemini CLI sends a JSON object to the hook's `stdin`.
- **Output**: The hook sends a JSON object (or plain text) to `stdout`.
- **Exit Codes**: Used to signal success or blocking errors.
### Exit Code Behavior
| Exit Code | Meaning | Behavior |
| :-------- | :----------------- | :---------------------------------------------------------------------------------------------- |
| `0` | **Success** | `stdout` is parsed as JSON. If parsing fails, it's treated as a `systemMessage`. |
| `2` | **Blocking Error** | Interrupts the current operation. `stderr` is shown to the agent (for tool events) or the user. |
| Other | **Warning** | Execution continues. `stderr` is logged as a non-blocking warning. |
- **Communication**: `stdin` for Input (JSON), `stdout` for Output (JSON), and
`stderr` for logs and feedback.
- **Exit codes**:
- `0`: Success. `stdout` is parsed as JSON. **Preferred for all logic.**
- `2`: System Block. The action is blocked; `stderr` is used as the rejection
reason.
- `Other`: Warning. A non-fatal failure occurred; the CLI continues with a
warning.
- **Silence is Mandatory**: Your script **must not** print any plain text to
`stdout` other than the final JSON.
---
## Input Schema (`stdin`)
## Base input schema
Every hook receives a base JSON object. Extra fields are added depending on the
specific event.
All hooks receive these common fields via `stdin`:
### Base Fields (All Events)
| Field | Type | Description |
| :---------------- | :------- | :---------------------------------------------------- |
| `session_id` | `string` | Unique identifier for the current CLI session. |
| `transcript_path` | `string` | Path to the session's JSON transcript (if available). |
| `cwd` | `string` | The current working directory. |
| `hook_event_name` | `string` | The name of the firing event (e.g., `BeforeTool`). |
| `timestamp` | `string` | ISO 8601 timestamp of the event. |
### Event-Specific Fields
#### Tool Events (`BeforeTool`, `AfterTool`)
- `tool_name`: (`string`) The internal name of the tool (e.g., `write_file`,
`run_shell_command`).
- `tool_input`: (`object`) The arguments passed to the tool.
- `tool_response`: (`object`, **AfterTool only**) The raw output from the tool
execution.
- `mcp_context`: (`object`, **optional**) Present only for MCP tool invocations.
Contains server identity information:
- `server_name`: (`string`) The configured name of the MCP server.
- `tool_name`: (`string`) The original tool name from the MCP server.
- `command`: (`string`, optional) For stdio transport, the command used to
start the server.
- `args`: (`string[]`, optional) For stdio transport, the command arguments.
- `cwd`: (`string`, optional) For stdio transport, the working directory.
- `url`: (`string`, optional) For SSE/HTTP transport, the server URL.
- `tcp`: (`string`, optional) For WebSocket transport, the TCP address.
#### Agent Events (`BeforeAgent`, `AfterAgent`)
- `prompt`: (`string`) The user's submitted prompt.
- `prompt_response`: (`string`, **AfterAgent only**) The final response text
from the model.
- `stop_hook_active`: (`boolean`, **AfterAgent only**) Indicates if a stop hook
is already handling a continuation.
#### Model Events (`BeforeModel`, `AfterModel`, `BeforeToolSelection`)
- `llm_request`: (`LLMRequest`) A stable representation of the outgoing request.
See [Stable Model API](#stable-model-api).
- `llm_response`: (`LLMResponse`, **AfterModel only**) A stable representation
of the incoming response.
#### Session & Notification Events
- `source`: (`startup` | `resume` | `clear`, **SessionStart only**) The trigger
source.
- `reason`: (`exit` | `clear` | `logout` | `prompt_input_exit` | `other`,
**SessionEnd only**) The reason for session end.
- `trigger`: (`manual` | `auto`, **PreCompress only**) What triggered the
compression event.
- `notification_type`: (`ToolPermission`, **Notification only**) The type of
notification being fired.
- `message`: (`string`, **Notification only**) The notification message.
- `details`: (`object`, **Notification only**) Payload-specific details for the
notification.
```typescript
{
"session_id": string, // Unique ID for the current session
"transcript_path": string, // Absolute path to session transcript JSON
"cwd": string, // Current working directory
"hook_event_name": string, // The firing event (e.g. "BeforeTool")
"timestamp": string // ISO 8601 execution time
}
```
---
## Output Schema (`stdout`)
## Common output fields
If the hook exits with `0`, the CLI attempts to parse `stdout` as JSON.
Most hooks support these fields in their `stdout` JSON:
### Common Output Fields
| Field | Type | Description |
| :--------------- | :-------- | :----------------------------------------------------------------------------- |
| `systemMessage` | `string` | Displayed immediately to the user in the terminal. |
| `suppressOutput` | `boolean` | If `true`, hides internal hook metadata from logs/telemetry. |
| `continue` | `boolean` | If `false`, stops the entire agent loop immediately. |
| `stopReason` | `string` | Displayed to the user when `continue` is `false`. |
| `decision` | `string` | `"allow"` or `"deny"` (alias `"block"`). Specific impact depends on the event. |
| `reason` | `string` | The feedback/error message provided when a `decision` is `"deny"`. |
| Field | Type | Description |
| :------------------- | :-------- | :------------------------------------------------------------------------------------- |
| `decision` | `string` | One of: `allow`, `deny`, `block`, `ask`, `approve`. |
| `reason` | `string` | Explanation shown to the **agent** when a decision is `deny` or `block`. |
| `systemMessage` | `string` | Message displayed in Gemini CLI terminal to provide warning or context to the **user** |
| `continue` | `boolean` | If `false`, immediately terminates the agent loop for this turn. |
| `stopReason` | `string` | Message shown to the user when `continue` is `false`. |
| `suppressOutput` | `boolean` | If `true`, the hook execution is hidden from the CLI transcript. |
| `hookSpecificOutput` | `object` | Container for event-specific data (see below). |
---
### `hookSpecificOutput` Reference
## Tool hooks
| Field | Supported Events | Description |
| :------------------ | :----------------------------------------- | :-------------------------------------------------------------------------------- |
| `additionalContext` | `SessionStart`, `BeforeAgent`, `AfterTool` | Appends text directly to the agent's context. |
| `llm_request` | `BeforeModel` | A `Partial<LLMRequest>` to override parameters of the outgoing call. |
| `llm_response` | `BeforeModel` | A **full** `LLMResponse` to bypass the model and provide a synthetic result. |
| `llm_response` | `AfterModel` | A `Partial<LLMResponse>` to modify the model's response before the agent sees it. |
| `toolConfig` | `BeforeToolSelection` | Object containing `mode` (`AUTO`/`ANY`/`NONE`) and `allowedFunctionNames`. |
### Matchers and tool names
For `BeforeTool` and `AfterTool` events, the `matcher` field in your settings is
compared against the name of the tool being executed.
- **Built-in Tools**: You can match any built-in tool (e.g., `read_file`,
`run_shell_command`). See the [Tools Reference](/docs/tools) for a full list
of available tool names.
- **MCP Tools**: Tools from MCP servers follow the naming pattern
`mcp__<server_name>__<tool_name>`.
- **Regex Support**: Matchers support regular expressions (e.g.,
`matcher: "read_.*"` matches all file reading tools).
### `BeforeTool`
Fires before a tool is invoked. Used for argument validation, security checks,
and parameter rewriting.
- **Input Fields**:
- `tool_name`: (`string`) The name of the tool being called.
- `tool_input`: (`object`) The raw arguments generated by the model.
- `mcp_context`: (`object`) Optional metadata for MCP-based tools.
- **Relevant Output Fields**:
- `decision`: Set to `"deny"` (or `"block"`) to prevent the tool from
executing.
- `reason`: Required if denied. This text is sent **to the agent** as a tool
error, allowing it to respond or retry.
- `hookSpecificOutput.tool_input`: An object that **merges with and
overrides** the model's arguments before execution.
- `continue`: Set to `false` to **kill the entire agent loop** immediately.
- **Exit Code 2 (Block Tool)**: Prevents execution. Uses `stderr` as the
`reason` sent to the agent. **The turn continues.**
### `AfterTool`
Fires after a tool executes. Used for result auditing, context injection, or
hiding sensitive output from the agent.
- **Input Fields**:
- `tool_name`: (`string`)
- `tool_input`: (`object`) The original arguments.
- `tool_response`: (`object`) The result containing `llmContent`,
`returnDisplay`, and optional `error`.
- `mcp_context`: (`object`)
- **Relevant Output Fields**:
- `decision`: Set to `"deny"` to hide the real tool output from the agent.
- `reason`: Required if denied. This text **replaces** the tool result sent
back to the model.
- `hookSpecificOutput.additionalContext`: Text that is **appended** to the
tool result for the agent.
- `continue`: Set to `false` to **kill the entire agent loop** immediately.
- **Exit Code 2 (Block Result)**: Hides the tool result. Uses `stderr` as the
replacement content sent to the agent. **The turn continues.**
---
## Agent hooks
### `BeforeAgent`
Fires after a user submits a prompt, but before the agent begins planning. Used
for prompt validation or injecting dynamic context.
- **Input Fields**:
- `prompt`: (`string`) The original text submitted by the user.
- **Relevant Output Fields**:
- `hookSpecificOutput.additionalContext`: Text that is **appended** to the
prompt for this turn only.
- `decision`: Set to `"deny"` to block the turn and **discard the user's
message** (it will not appear in history).
- `continue`: Set to `false` to block the turn but **save the message to
history**.
- `reason`: Required if denied or stopped.
- **Exit Code 2 (Block Turn)**: Aborts the turn and erases the prompt from
context. Same as `decision: "deny"`.
### `AfterAgent`
Fires once per turn after the model generates its final response. Primary use
case is response validation and automatic retries.
- **Input Fields**:
- `prompt`: (`string`) The user's original request.
- `prompt_response`: (`string`) The final text generated by the agent.
- `stop_hook_active`: (`boolean`) Indicates if this hook is already running as
part of a retry sequence.
- **Relevant Output Fields**:
- `decision`: Set to `"deny"` to **reject the response** and force a retry.
- `reason`: Required if denied. This text is sent **to the agent as a new
prompt** to request a correction.
- `continue`: Set to `false` to **stop the session** without retrying.
- **Exit Code 2 (Retry)**: Rejects the response and triggers an automatic retry
turn using `stderr` as the feedback prompt.
---
## Model hooks
### `BeforeModel`
Fires before sending a request to the LLM. Operates on a stable, SDK-agnostic
request format.
- **Input Fields**:
- `llm_request`: (`object`) Contains `model`, `messages`, and `config`
(generation params).
- **Relevant Output Fields**:
- `hookSpecificOutput.llm_request`: An object that **overrides** parts of the
outgoing request (e.g., changing models or temperature).
- `hookSpecificOutput.llm_response`: A **Synthetic Response** object. If
provided, the CLI skips the LLM call entirely and uses this as the response.
- `decision`: Set to `"deny"` to block the request and abort the turn.
- **Exit Code 2 (Block Turn)**: Aborts the turn and skips the LLM call. Uses
`stderr` as the error message.
### `BeforeToolSelection`
Fires before the LLM decides which tools to call. Used to filter the available
toolset or force specific tool modes.
- **Input Fields**:
- `llm_request`: (`object`) Same format as `BeforeModel`.
- **Relevant Output Fields**:
- `hookSpecificOutput.toolConfig.mode`: (`"AUTO" | "ANY" | "NONE"`)
- `"NONE"`: Disables all tools (Wins over other hooks).
- `"ANY"`: Forces at least one tool call.
- `hookSpecificOutput.toolConfig.allowedFunctionNames`: (`string[]`) Whitelist
of tool names.
- **Union Strategy**: Multiple hooks' whitelists are **combined**.
- **Limitations**: Does **not** support `decision`, `continue`, or
`systemMessage`.
### `AfterModel`
Fires immediately after an LLM response chunk is received. Used for real-time
redaction or PII filtering.
- **Input Fields**:
- `llm_request`: (`object`) The original request.
- `llm_response`: (`object`) The model's response (or a single chunk during
streaming).
- **Relevant Output Fields**:
- `hookSpecificOutput.llm_response`: An object that **replaces** the model's
response chunk.
- `decision`: Set to `"deny"` to discard the response chunk and block the
turn.
- `continue`: Set to `false` to **kill the entire agent loop** immediately.
- **Note on Streaming**: Fired for **every chunk** generated by the model.
Modifying the response only affects the current chunk.
- **Exit Code 2 (Block Response)**: Aborts the turn and discards the model's
output. Uses `stderr` as the error message.
---
## Lifecycle & system hooks
### `SessionStart`
Fires on application startup, resuming a session, or after a `/clear` command.
Used for loading initial context.
- **Input fields**:
- `source`: (`"startup" | "resume" | "clear"`)
- **Relevant output fields**:
- `hookSpecificOutput.additionalContext`: (`string`)
- **Interactive**: Injected as the first turn in history.
- **Non-interactive**: Prepended to the user's prompt.
- `systemMessage`: Shown at the start of the session.
- **Advisory only**: `continue` and `decision` fields are **ignored**. Startup
is never blocked.
### `SessionEnd`
Fires when the CLI exits or a session is cleared. Used for cleanup or final
telemetry.
- **Input Fields**:
- `reason`: (`"exit" | "clear" | "logout" | "prompt_input_exit" | "other"`)
- **Relevant Output Fields**:
- `systemMessage`: Displayed to the user during shutdown.
- **Best Effort**: The CLI **will not wait** for this hook to complete and
ignores all flow-control fields (`continue`, `decision`).
### `Notification`
Fires when the CLI emits a system alert (e.g., Tool Permissions). Used for
external logging or cross-platform alerts.
- **Input Fields**:
- `notification_type`: (`"ToolPermission"`)
- `message`: Summary of the alert.
- `details`: JSON object with alert-specific metadata (e.g., tool name, file
path).
- **Relevant Output Fields**:
- `systemMessage`: Displayed alongside the system alert.
- **Observability Only**: This hook **cannot** block alerts or grant permissions
automatically. Flow-control fields are ignored.
### `PreCompress`
Fires before the CLI summarizes history to save tokens. Used for logging or
state saving.
- **Input Fields**:
- `trigger`: (`"auto" | "manual"`)
- **Relevant Output Fields**:
- `systemMessage`: Displayed to the user before compression.
- **Advisory Only**: Fired asynchronously. It **cannot** block or modify the
compression process. Flow-control fields are ignored.
---
## Stable Model API
Gemini CLI uses a decoupled format for model interactions to ensure hooks remain
stable even if the underlying Gemini SDK changes.
Gemini CLI uses these structures to ensure hooks don't break across SDK updates.
### `LLMRequest` Object
Used in `BeforeModel` and `BeforeToolSelection`.
> 💡 **Note**: In v1, model hooks are primarily text-focused. Non-text parts
> (like images or function calls) provided in the `content` array will be
> simplified to their string representation by the translator.
**LLMRequest**:
```typescript
{
"model": string,
"messages": Array<{
"role": "user" | "model" | "system",
"content": string | Array<{ "type": string, [key: string]: any }>
"content": string // Non-text parts are filtered out for hooks
}>,
"config"?: {
"temperature"?: number,
"maxOutputTokens"?: number,
"topP"?: number,
"topK"?: number
},
"toolConfig"?: {
"mode"?: "AUTO" | "ANY" | "NONE",
"allowedFunctionNames"?: string[]
}
"config": { "temperature": number, ... },
"toolConfig": { "mode": string, "allowedFunctionNames": string[] }
}
```
### `LLMResponse` Object
Used in `AfterModel` and as a synthetic response in `BeforeModel`.
**LLMResponse**:
```typescript
{
"text"?: string,
"candidates": Array<{
"content": {
"role": "model",
"parts": string[]
},
"finishReason"?: "STOP" | "MAX_TOKENS" | "SAFETY" | "RECITATION" | "OTHER",
"index"?: number,
"safetyRatings"?: Array<{
"category": string,
"probability": string,
"blocked"?: boolean
}>
"content": { "role": "model", "parts": string[] },
"finishReason": string
}>,
"usageMetadata"?: {
"promptTokenCount"?: number,
"candidatesTokenCount"?: number,
"totalTokenCount"?: number
}
"usageMetadata": { "totalTokenCount": number }
}
```

File diff suppressed because it is too large Load Diff