chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,710 @@
|
||||
# Hooks Best Practices
|
||||
|
||||
This guide covers security considerations, performance optimization, debugging
|
||||
techniques, and privacy considerations for developing and deploying hooks in
|
||||
Gemini CLI.
|
||||
|
||||
## Performance
|
||||
|
||||
### Keep hooks fast
|
||||
|
||||
Hooks run synchronously—slow hooks delay the agent loop. Optimize for speed by
|
||||
using parallel operations:
|
||||
|
||||
```javascript
|
||||
// Sequential operations are slower
|
||||
const data1 = await fetch(url1).then((r) => r.json());
|
||||
const data2 = await fetch(url2).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());
|
||||
|
||||
// Wait for all results
|
||||
const [data1, data2] = await Promise.all([p1, p2]);
|
||||
```
|
||||
|
||||
### Cache expensive operations
|
||||
|
||||
Store results between invocations to avoid repeated computation, especially for
|
||||
hooks that run frequently (like `BeforeTool` or `AfterModel`).
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CACHE_FILE = '.gemini/hook-cache.json';
|
||||
|
||||
function readCache() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(data) {
|
||||
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cache = readCache();
|
||||
const cacheKey = `tool-list-${(Date.now() / 3600000) | 0}`; // Hourly cache
|
||||
|
||||
if (cache[cacheKey]) {
|
||||
// Write JSON to stdout
|
||||
console.log(JSON.stringify(cache[cacheKey]));
|
||||
return;
|
||||
}
|
||||
|
||||
// Expensive operation
|
||||
const result = await computeExpensiveResult();
|
||||
cache[cacheKey] = result;
|
||||
writeCache(cache);
|
||||
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
```
|
||||
|
||||
### Use appropriate events
|
||||
|
||||
Choose hook events that match your use case to avoid unnecessary execution.
|
||||
|
||||
- **`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. This saves the overhead of
|
||||
spawning a process for irrelevant events.
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "validate-writes",
|
||||
"type": "command",
|
||||
"command": "./validate.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Optimize JSON parsing
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
LOG_FILE=".gemini/hooks/debug.log"
|
||||
|
||||
# Log with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
input=$(cat)
|
||||
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
|
||||
|
||||
Error messages on stderr are surfaced appropriately based on exit codes:
|
||||
|
||||
```javascript
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
### Test hooks independently
|
||||
|
||||
Run hook scripts manually with sample JSON input to verify they behave as
|
||||
expected before hooking them up to the CLI.
|
||||
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
# Create test input
|
||||
cat > test-input.json << 'EOF'
|
||||
{
|
||||
"session_id": "test-123",
|
||||
"cwd": "/tmp/test",
|
||||
"hook_event_name": "BeforeTool",
|
||||
"tool_name": "write_file",
|
||||
"tool_input": {
|
||||
"file_path": "test.txt",
|
||||
"content": "Test content"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Test the hook
|
||||
cat test-input.json | .gemini/hooks/my-hook.sh
|
||||
|
||||
# Check exit code
|
||||
echo "Exit code: $?"
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
# Create test input
|
||||
@"
|
||||
{
|
||||
"session_id": "test-123",
|
||||
"cwd": "C:\\temp\\test",
|
||||
"hook_event_name": "BeforeTool",
|
||||
"tool_name": "write_file",
|
||||
"tool_input": {
|
||||
"file_path": "test.txt",
|
||||
"content": "Test content"
|
||||
}
|
||||
}
|
||||
"@ | Out-File -FilePath test-input.json -Encoding utf8
|
||||
|
||||
# Test the hook
|
||||
Get-Content test-input.json | .\.gemini\hooks\my-hook.ps1
|
||||
|
||||
# Check exit code
|
||||
Write-Host "Exit code: $LASTEXITCODE"
|
||||
```
|
||||
|
||||
### Check exit codes
|
||||
|
||||
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
|
||||
|
||||
# Hook logic
|
||||
if process_input; then
|
||||
echo '{"decision": "allow"}'
|
||||
exit 0
|
||||
else
|
||||
echo "Critical validation failure" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
```
|
||||
|
||||
### Enable telemetry
|
||||
|
||||
Hook execution is logged when `telemetry.logPrompts` is enabled. You can view
|
||||
these logs to debug execution flow.
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"logPrompts": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use hook panel
|
||||
|
||||
The `/hooks panel` command inside the CLI shows execution status and recent
|
||||
output:
|
||||
|
||||
```bash
|
||||
/hooks panel
|
||||
```
|
||||
|
||||
Check for:
|
||||
|
||||
- Hook execution counts
|
||||
- Recent successes/failures
|
||||
- Error messages
|
||||
- Execution timing
|
||||
|
||||
## Development
|
||||
|
||||
### Start simple
|
||||
|
||||
Begin with basic logging hooks before implementing complex logic:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Simple logging hook to understand input structure
|
||||
input=$(cat)
|
||||
echo "$input" >> .gemini/hook-inputs.log
|
||||
# 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.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```bash
|
||||
# Fragile text parsing
|
||||
tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')
|
||||
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```bash
|
||||
# Robust JSON parsing
|
||||
tool_name=$(echo "$input" | jq -r '.tool_name')
|
||||
|
||||
```
|
||||
|
||||
### Make scripts executable
|
||||
|
||||
Always make hook scripts executable on macOS/Linux:
|
||||
|
||||
```bash
|
||||
chmod +x .gemini/hooks/*.sh
|
||||
chmod +x .gemini/hooks/*.js
|
||||
|
||||
```
|
||||
|
||||
**Windows Note**: On Windows, PowerShell scripts (`.ps1`) don't use `chmod`, but
|
||||
you may need to ensure your execution policy allows them to run (for example,
|
||||
`Set-ExecutionPolicy RemoteSigned -Scope CurrentUser`).
|
||||
|
||||
### Version control
|
||||
|
||||
Commit hooks to share with your team:
|
||||
|
||||
```bash
|
||||
git add .gemini/hooks/
|
||||
git add .gemini/settings.json
|
||||
|
||||
```
|
||||
|
||||
**`.gitignore` considerations:**
|
||||
|
||||
```gitignore
|
||||
# Ignore hook cache and logs
|
||||
.gemini/hook-cache.json
|
||||
.gemini/hook-debug.log
|
||||
.gemini/memory/session-*.jsonl
|
||||
|
||||
# Keep hook scripts
|
||||
!.gemini/hooks/*.sh
|
||||
!.gemini/hooks/*.js
|
||||
|
||||
```
|
||||
|
||||
## Hook security
|
||||
|
||||
### Threat Model
|
||||
|
||||
Understanding where hooks come from and what they can do is critical for secure
|
||||
usage.
|
||||
|
||||
| Hook Source | Description |
|
||||
| :---------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **System** | Configured by system administrators (for example, `/etc/gemini-cli/settings.json`, `/Library/...`). Assumed to be the **safest**. |
|
||||
| **User** (`~/.gemini/...`) | Configured by you. You are responsible for ensuring they are safe. |
|
||||
| **Extensions** | You explicitly approve and install these. Security depends on the extension source (integrity). |
|
||||
| **Project** (`./.gemini/...`) | **Untrusted by default.** Safest in trusted internal repos; higher risk in third-party/public repos. |
|
||||
|
||||
#### Project Hook Security
|
||||
|
||||
When you open a project with hooks defined in `.gemini/settings.json`:
|
||||
|
||||
1. **Detection**: Gemini CLI detects the hooks.
|
||||
2. **Identification**: A unique identity is generated for each hook based on its
|
||||
`name` and `command`.
|
||||
3. **Warning**: If this specific hook identity has not been seen before, a
|
||||
**warning** is displayed.
|
||||
4. **Execution**: The hook is executed (unless specific security settings block
|
||||
it).
|
||||
5. **Trust**: The hook is marked as "trusted" for this project.
|
||||
|
||||
> **Modification detection**: If the `command` string of a project hook is
|
||||
> changed (for example, 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
|
||||
|
||||
| Risk | Description |
|
||||
| :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Arbitrary Code Execution** | Hooks run as your user. They can do anything you can do (delete files, install software). |
|
||||
| **Data Exfiltration** | A hook could read your input (prompts), output (code), or environment variables (`GEMINI_API_KEY`) and send them to a remote server. |
|
||||
| **Prompt Injection** | Malicious content in a file or web page could trick an LLM into running a tool that triggers a hook in an unexpected way. |
|
||||
|
||||
### Mitigation Strategies
|
||||
|
||||
#### Verify the source
|
||||
|
||||
**Verify the source** of any project hooks or extensions before enabling them.
|
||||
|
||||
- For open-source projects, a quick review of the hook scripts is recommended.
|
||||
- For extensions, ensure you trust the author or publisher (for example,
|
||||
verified publishers, well-known community members).
|
||||
- Be cautious with obfuscated scripts or compiled binaries from unknown sources.
|
||||
|
||||
#### Sanitize environment
|
||||
|
||||
Hooks inherit the environment of Gemini CLI process, which may include sensitive
|
||||
API keys. Gemini CLI provides a
|
||||
[redaction system](../reference/configuration.md#environment-variable-redaction)
|
||||
that automatically filters variables matching sensitive patterns (for example,
|
||||
`KEY`, `TOKEN`).
|
||||
|
||||
> **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.
|
||||
|
||||
**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,
|
||||
"allowed": ["MY_REQUIRED_TOOL_KEY"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
**Windows Note**: On Windows, ensure your execution policy allows running
|
||||
scripts (for example, `Get-ExecutionPolicy`).
|
||||
|
||||
**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.
|
||||
|
||||
### Validate all inputs
|
||||
|
||||
Never trust data from hooks without validation. Hook inputs often come from the
|
||||
LLM or user prompts, which can be manipulated.
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
input=$(cat)
|
||||
|
||||
# Validate JSON structure
|
||||
if ! echo "$input" | jq empty 2>/dev/null; then
|
||||
echo "Invalid JSON input" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate tool_name explicitly
|
||||
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
||||
if [[ "$tool_name" != "write_file" && "$tool_name" != "read_file" ]]; then
|
||||
echo "Unexpected tool: $tool_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Use timeouts
|
||||
|
||||
Prevent denial-of-service (hanging agents) by enforcing timeouts. Gemini CLI
|
||||
defaults to 60 seconds, but you should set stricter limits for fast hooks.
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "fast-validator",
|
||||
"type": "command",
|
||||
"command": "./hooks/validate.sh",
|
||||
"timeout": 5000 // 5 seconds
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Limit permissions
|
||||
|
||||
Run hooks with minimal required permissions:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Don't run as root
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "Hook should not run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check file permissions before writing
|
||||
if [ -w "$file_path" ]; then
|
||||
# Safe to write
|
||||
else
|
||||
echo "Insufficient permissions" >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Example: Secret Scanner
|
||||
|
||||
Use `BeforeTool` hooks to prevent committing sensitive data. This is a powerful
|
||||
pattern for enhancing security in your workflow.
|
||||
|
||||
```javascript
|
||||
const SECRET_PATTERNS = [
|
||||
/api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
|
||||
/password\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/i,
|
||||
/secret\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
|
||||
/AKIA[0-9A-Z]{16}/, // AWS access key
|
||||
/ghp_[a-zA-Z0-9]{36}/, // GitHub personal access token
|
||||
/sk-[a-zA-Z0-9]{48}/, // OpenAI API key
|
||||
];
|
||||
|
||||
function containsSecret(content) {
|
||||
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
|
||||
}
|
||||
```
|
||||
|
||||
## Privacy considerations
|
||||
|
||||
Hook inputs and outputs may contain sensitive information.
|
||||
|
||||
### What data is collected
|
||||
|
||||
Hook telemetry may include inputs (prompts, code) and outputs (decisions,
|
||||
reasons) unless disabled.
|
||||
|
||||
### Privacy settings
|
||||
|
||||
**Disable PII logging:** If you are working with sensitive data, disable prompt
|
||||
logging in your settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"logPrompts": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Suppress Output:** Individual hooks can request their metadata be hidden from
|
||||
logs and telemetry by returning `"suppressOutput": true` in their JSON response.
|
||||
|
||||
> **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 JSON or writing
|
||||
to stderr.
|
||||
@@ -0,0 +1,168 @@
|
||||
# Gemini CLI hooks
|
||||
|
||||
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.
|
||||
|
||||
## What are hooks?
|
||||
|
||||
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.
|
||||
|
||||
With hooks, you can:
|
||||
|
||||
- **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.
|
||||
|
||||
### Getting started
|
||||
|
||||
- **[Writing hooks guide](../hooks/writing-hooks.md)**: A tutorial on creating
|
||||
your first hook with comprehensive examples.
|
||||
- **[Best practices](../hooks/best-practices.md)**: Guidelines on security,
|
||||
performance, and debugging.
|
||||
- **[Hooks reference](../hooks/reference.md)**: The definitive technical
|
||||
specification of I/O schemas and exit codes.
|
||||
|
||||
## Core concepts
|
||||
|
||||
### Hook events
|
||||
|
||||
Hooks are triggered by specific events in Gemini CLI's lifecycle.
|
||||
|
||||
| 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 |
|
||||
|
||||
### Global mechanics
|
||||
|
||||
Understanding these core principles is essential for building robust hooks.
|
||||
|
||||
#### Strict JSON requirements (The "Golden Rule")
|
||||
|
||||
Hooks communicate via `stdin` (Input) and `stdout` (Output).
|
||||
|
||||
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 (for
|
||||
example, `echo "debug" >&2`). Gemini CLI captures `stderr` but never attempts
|
||||
to parse it as JSON.
|
||||
|
||||
#### 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 (for example, `{"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**. (for example, `"write_.*"`).
|
||||
- **Lifecycle events**: Matchers are **Exact Strings**. (for example,
|
||||
`"startup"`).
|
||||
- **Wildcards**: `"*"` or `""` (empty string) matches all occurrences.
|
||||
|
||||
## Configuration
|
||||
|
||||
Hooks 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
|
||||
{
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "security-check",
|
||||
"type": "command",
|
||||
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/security.sh",
|
||||
"timeout": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Hook configuration fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------------ | :----- | :-------- | :------------------------------------------------------------------- |
|
||||
| `type` | string | **Yes** | The execution engine. Currently only `"command"` is supported. |
|
||||
| `command` | string | **Yes\*** | The shell command to execute. (Required when `type` is `"command"`). |
|
||||
| `name` | string | No | A friendly name for identifying the hook in logs and CLI commands. |
|
||||
| `timeout` | number | No | Execution timeout in milliseconds (default: 60000). |
|
||||
| `description` | string | No | A brief explanation of the hook's purpose. |
|
||||
|
||||
---
|
||||
|
||||
### Environment variables
|
||||
|
||||
Hooks are executed with a sanitized environment.
|
||||
|
||||
- `GEMINI_PROJECT_DIR`: The absolute path to the project root.
|
||||
- `GEMINI_PLANS_DIR`: The absolute path to the plans directory.
|
||||
- `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
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!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
|
||||
(for example, via `git pull`), it is treated as a **new, untrusted hook** and
|
||||
you will be warned before it executes.
|
||||
|
||||
See [Security Considerations](../hooks/best-practices.md#using-hooks-securely)
|
||||
for a detailed threat model.
|
||||
|
||||
## Managing hooks
|
||||
|
||||
Use the CLI commands to manage hooks without editing JSON manually:
|
||||
|
||||
- **View hooks:** `/hooks panel`
|
||||
- **Enable/Disable all:** `/hooks enable-all` or `/hooks disable-all`
|
||||
- **Toggle individual:** `/hooks enable <name>` or `/hooks disable <name>`
|
||||
@@ -0,0 +1,330 @@
|
||||
# Hooks reference
|
||||
|
||||
This document provides the technical specification for Gemini CLI hooks,
|
||||
including JSON schemas and API details.
|
||||
|
||||
## Global hook mechanics
|
||||
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## Configuration schema
|
||||
|
||||
Hooks are defined in `settings.json` within the `hooks` object. Each event (for
|
||||
example, `BeforeTool`) contains an array of **hook definitions**.
|
||||
|
||||
### Hook definition
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :----------- | :-------- | :------- | :-------------------------------------------------------------------------------------- |
|
||||
| `matcher` | `string` | No | A regex (for tools) or exact string (for lifecycle) to filter when the hook runs. |
|
||||
| `sequential` | `boolean` | No | If `true`, hooks in this group run one after another. If `false`, they run in parallel. |
|
||||
| `hooks` | `array` | **Yes** | An array of **hook configurations**. |
|
||||
|
||||
### Hook configuration
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------------ | :------- | :-------- | :------------------------------------------------------------------- |
|
||||
| `type` | `string` | **Yes** | The execution engine. Currently only `"command"` is supported. |
|
||||
| `command` | `string` | **Yes\*** | The shell command to execute. (Required when `type` is `"command"`). |
|
||||
| `name` | `string` | No | A friendly name for identifying the hook in logs and CLI commands. |
|
||||
| `timeout` | `number` | No | Execution timeout in milliseconds (default: 60000). |
|
||||
| `description` | `string` | No | A brief explanation of the hook's purpose. |
|
||||
|
||||
---
|
||||
|
||||
## Base input schema
|
||||
|
||||
All hooks receive these common fields via `stdin`:
|
||||
|
||||
```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 (for example "BeforeTool")
|
||||
"timestamp": string // ISO 8601 execution time
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common output fields
|
||||
|
||||
Most hooks support these fields in their `stdout` JSON:
|
||||
|
||||
| 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"`. |
|
||||
|
||||
---
|
||||
|
||||
## Tool hooks
|
||||
|
||||
### 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 (for example, `read_file`,
|
||||
`run_shell_command`). See the [Tools Reference](../reference/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 (for example,
|
||||
`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.
|
||||
- `original_request_name`: (`string`) The original name of the tool being
|
||||
called, if this is a tail tool call.
|
||||
- **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`)
|
||||
- `original_request_name`: (`string`) The original name of the tool being
|
||||
called, if this is a tail tool call.
|
||||
- **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.
|
||||
- `hookSpecificOutput.tailToolCallRequest`: (`{ name: string, args: object }`)
|
||||
A request to execute another tool immediately after this one. The result of
|
||||
this "tail call" will replace the original tool's response. Ideal for
|
||||
programmatic tool routing.
|
||||
- `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.
|
||||
- `hookSpecificOutput.clearContext`: If `true`, clears conversation history
|
||||
(LLM memory) while preserving UI display.
|
||||
- **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 (for example, 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 (for example, 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 (for example, 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 these structures to ensure hooks don't break across SDK updates.
|
||||
|
||||
**LLMRequest**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
"model": string,
|
||||
"messages": Array<{
|
||||
"role": "user" | "model" | "system",
|
||||
"content": string // Non-text parts are filtered out for hooks
|
||||
}>,
|
||||
"config": { "temperature": number, ... },
|
||||
"toolConfig": { "mode": string, "allowedFunctionNames": string[] }
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**LLMResponse**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
"candidates": Array<{
|
||||
"content": { "role": "model", "parts": string[] },
|
||||
"finishReason": string
|
||||
}>,
|
||||
"usageMetadata": { "totalTokenCount": number }
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,474 @@
|
||||
# Writing hooks for Gemini CLI
|
||||
|
||||
This guide will walk you through creating hooks for Gemini CLI, from a simple
|
||||
logging hook to a comprehensive workflow assistant.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start, make sure you have:
|
||||
|
||||
- Gemini CLI installed and configured
|
||||
- Basic understanding of shell scripting or JavaScript/Node.js
|
||||
- Familiarity with JSON for hook input/output
|
||||
|
||||
## Quick start
|
||||
|
||||
Let's create a simple hook that logs all tool executions to understand the
|
||||
basics.
|
||||
|
||||
**Crucial Rule:** Always write logs to `stderr`. Write only the final JSON to
|
||||
`stdout`.
|
||||
|
||||
### Step 1: Create your hook script
|
||||
|
||||
Create a directory for hooks and a simple logging script.
|
||||
|
||||
> **Note**:
|
||||
>
|
||||
> This example uses `jq` to parse JSON. If you don't have it installed, you can
|
||||
> perform similar logic using Node.js or Python.
|
||||
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
mkdir -p .gemini/hooks
|
||||
cat > .gemini/hooks/log-tools.sh << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Read hook input from stdin
|
||||
input=$(cat)
|
||||
|
||||
# Extract tool name (requires jq)
|
||||
tool_name=$(echo "$input" | jq -r '.tool_name')
|
||||
|
||||
# Log to stderr (visible in terminal if hook fails, or captured in logs)
|
||||
echo "Logging tool: $tool_name" >&2
|
||||
|
||||
# Log to file
|
||||
echo "[$(date)] Tool executed: $tool_name" >> .gemini/tool-log.txt
|
||||
|
||||
# Return success (exit 0) with empty JSON
|
||||
echo "{}"
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
chmod +x .gemini/hooks/log-tools.sh
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path ".gemini\hooks"
|
||||
@"
|
||||
# Read hook input from stdin
|
||||
`$inputJson = `$input | Out-String | ConvertFrom-Json
|
||||
|
||||
# Extract tool name
|
||||
`$toolName = `$inputJson.tool_name
|
||||
|
||||
# Log to stderr (visible in terminal if hook fails, or captured in logs)
|
||||
[Console]::Error.WriteLine("Logging tool: `$toolName")
|
||||
|
||||
# Log to file
|
||||
"[`$(Get-Date -Format 'o')] Tool executed: `$toolName" | Out-File -FilePath ".gemini\tool-log.txt" -Append -Encoding utf8
|
||||
|
||||
# Return success with empty JSON
|
||||
"{}"
|
||||
"@ | Out-File -FilePath ".gemini\hooks\log-tools.ps1" -Encoding utf8
|
||||
```
|
||||
|
||||
## Exit Code Strategies
|
||||
|
||||
There are two ways to control or block an action in Gemini CLI:
|
||||
|
||||
| Strategy | Exit Code | Implementation | Best For |
|
||||
| :------------------------- | :-------- | :----------------------------------------------------------------- | :---------------------------------------------------------- |
|
||||
| **Structured (Idiomatic)** | `0` | Return a JSON object like `{"decision": "deny", "reason": "..."}`. | Production hooks, custom user feedback, and complex logic. |
|
||||
| **Emergency Brake** | `2` | Print the error message to `stderr` and exit. | Simple security gates, script errors, or rapid prototyping. |
|
||||
|
||||
## Practical examples
|
||||
|
||||
### Security: Block secrets in commits
|
||||
|
||||
Prevent committing files containing API keys or passwords. Note that we use
|
||||
**Exit Code 0** to provide a structured denial message to the agent.
|
||||
|
||||
**`.gemini/hooks/block-secrets.sh`:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
input=$(cat)
|
||||
|
||||
# Extract content being written
|
||||
content=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // ""')
|
||||
|
||||
# Check for secrets
|
||||
if echo "$content" | grep -qE 'api[_-]?key|password|secret'; then
|
||||
# Log to stderr
|
||||
echo "Blocked potential secret" >&2
|
||||
|
||||
# Return structured denial to stdout
|
||||
cat <<EOF
|
||||
{
|
||||
"decision": "deny",
|
||||
"reason": "Security Policy: Potential secret detected in content.",
|
||||
"systemMessage": "🔒 Security scanner blocked operation"
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Allow
|
||||
echo '{"decision": "allow"}'
|
||||
exit 0
|
||||
```
|
||||
|
||||
### Dynamic context injection (Git History)
|
||||
|
||||
Add relevant project context before each agent interaction.
|
||||
|
||||
**`.gemini/hooks/inject-context.sh`:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Get recent git commits for context
|
||||
context=$(git log -5 --oneline 2>/dev/null || echo "No git history")
|
||||
|
||||
# Return as JSON
|
||||
cat <<EOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "BeforeAgent",
|
||||
"additionalContext": "Recent commits:\n$context"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
### RAG-based Tool Filtering (BeforeToolSelection)
|
||||
|
||||
Use `BeforeToolSelection` to intelligently reduce the tool space. This example
|
||||
uses a Node.js script to check the user's prompt and allow only relevant tools.
|
||||
|
||||
**`.gemini/hooks/filter-tools.js`:**
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
|
||||
async function main() {
|
||||
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
|
||||
const { llm_request } = input;
|
||||
|
||||
// Decoupled API: Access messages from llm_request
|
||||
const messages = llm_request.messages || [];
|
||||
const lastUserMessage = messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((m) => m.role === 'user');
|
||||
|
||||
if (!lastUserMessage) {
|
||||
console.log(JSON.stringify({})); // Do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
const text = lastUserMessage.content;
|
||||
const allowed = ['write_todos']; // Always allow memory
|
||||
|
||||
// Simple keyword matching
|
||||
if (text.includes('read') || text.includes('check')) {
|
||||
allowed.push('read_file', 'list_directory');
|
||||
}
|
||||
if (text.includes('test')) {
|
||||
allowed.push('run_shell_command');
|
||||
}
|
||||
|
||||
// If we found specific intent, filter tools. Otherwise allow all.
|
||||
if (allowed.length > 1) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'BeforeToolSelection',
|
||||
toolConfig: {
|
||||
mode: 'ANY', // Force usage of one of these tools (or AUTO)
|
||||
allowedFunctionNames: allowed,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.log(JSON.stringify({}));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
**`.gemini/settings.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"BeforeToolSelection": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "intent-filter",
|
||||
"type": "command",
|
||||
"command": "node .gemini/hooks/filter-tools.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **TIP**
|
||||
>
|
||||
> **Union Aggregation Strategy**: `BeforeToolSelection` is unique in that it
|
||||
> combines the results of all matching hooks. If you have multiple filtering
|
||||
> hooks, the agent will receive the **union** of all whitelisted tools. Only
|
||||
> using `mode: "NONE"` will override other hooks to disable all tools.
|
||||
|
||||
## Complete example: Smart Development Workflow Assistant
|
||||
|
||||
This comprehensive example demonstrates all hook events working together. We
|
||||
will build a system that maintains memory, filters tools, and checks for
|
||||
security.
|
||||
|
||||
### Architecture
|
||||
|
||||
1. **SessionStart**: Load project memories.
|
||||
2. **BeforeAgent**: Inject memories into context.
|
||||
3. **BeforeToolSelection**: Filter tools based on intent.
|
||||
4. **BeforeTool**: Scan for secrets.
|
||||
5. **AfterModel**: Record interactions.
|
||||
6. **AfterAgent**: Validate final response quality (Retry).
|
||||
7. **SessionEnd**: Consolidate memories.
|
||||
|
||||
### Configuration (`.gemini/settings.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "init",
|
||||
"type": "command",
|
||||
"command": "node .gemini/hooks/init.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"BeforeAgent": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "memory",
|
||||
"type": "command",
|
||||
"command": "node .gemini/hooks/inject-memories.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"BeforeToolSelection": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "filter",
|
||||
"type": "command",
|
||||
"command": "node .gemini/hooks/rag-filter.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "write_file",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "security",
|
||||
"type": "command",
|
||||
"command": "node .gemini/hooks/security.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AfterModel": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "record",
|
||||
"type": "command",
|
||||
"command": "node .gemini/hooks/record.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AfterAgent": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "validate",
|
||||
"type": "command",
|
||||
"command": "node .gemini/hooks/validate.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"matcher": "exit",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "save",
|
||||
"type": "command",
|
||||
"command": "node .gemini/hooks/consolidate.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hook Scripts
|
||||
|
||||
> **Note**: For brevity, these scripts use `console.error` for logging and
|
||||
> standard `console.log` for JSON output.
|
||||
|
||||
#### 1. Initialize (`init.js`)
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
// Initialize DB or resources
|
||||
console.error('Initializing assistant...');
|
||||
|
||||
// Output to user
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
systemMessage: '🧠 Smart Assistant Loaded',
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. Inject Memories (`inject-memories.js`)
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
|
||||
async function main() {
|
||||
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
|
||||
// Assume we fetch memories from a DB here
|
||||
const memories = '- [Memory] Always use TypeScript for this project.';
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'BeforeAgent',
|
||||
additionalContext: `\n## Relevant Memories\n${memories}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
main();
|
||||
```
|
||||
|
||||
#### 3. Security Check (`security.js`)
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const input = JSON.parse(fs.readFileSync(0));
|
||||
const content = input.tool_input.content || '';
|
||||
|
||||
if (content.includes('SECRET_KEY')) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
decision: 'deny',
|
||||
reason: 'Found SECRET_KEY in content',
|
||||
systemMessage: '🚨 Blocked sensitive commit',
|
||||
}),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ decision: 'allow' }));
|
||||
```
|
||||
|
||||
#### 4. Record Interaction (`record.js`)
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0));
|
||||
const { llm_request, llm_response } = input;
|
||||
const logFile = path.join(
|
||||
process.env.GEMINI_PROJECT_DIR,
|
||||
'.gemini/memory/session.jsonl',
|
||||
);
|
||||
|
||||
fs.appendFileSync(
|
||||
logFile,
|
||||
JSON.stringify({
|
||||
request: llm_request,
|
||||
response: llm_response,
|
||||
timestamp: new Date().toISOString(),
|
||||
}) + '\n',
|
||||
);
|
||||
|
||||
console.log(JSON.stringify({}));
|
||||
```
|
||||
|
||||
#### 5. Validate Response (`validate.js`)
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const input = JSON.parse(fs.readFileSync(0));
|
||||
const response = input.prompt_response;
|
||||
|
||||
// Example: Check if the agent forgot to include a summary
|
||||
if (!response.includes('Summary:')) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
decision: 'block', // Triggers an automatic retry turn
|
||||
reason: 'Your response is missing a Summary section. Please add one.',
|
||||
systemMessage: '🔄 Requesting missing summary...',
|
||||
}),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ decision: 'allow' }));
|
||||
```
|
||||
|
||||
#### 6. Consolidate Memories (`consolidate.js`)
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
// Logic to save final session state
|
||||
console.error('Consolidating memories for session end...');
|
||||
```
|
||||
|
||||
## Packaging as an extension
|
||||
|
||||
While project-level hooks are great for specific repositories, you can share
|
||||
your hooks across multiple projects by packaging them as a
|
||||
[Gemini CLI extension](../extensions/index.md). This provides version control,
|
||||
easy distribution, and centralized management.
|
||||
Reference in New Issue
Block a user