mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
docs(hooks): comprehensive update of hook documentation and specs (#16816)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user