2026-01-21 22:13:15 -05:00
# Hooks Best Practices
2025-12-03 10:57:05 -08:00
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
2026-01-02 15:22:30 -05:00
// Start requests concurrently
const p1 = fetch(url1).then((r) => r.json());
const p2 = fetch(url2).then((r) => r.json());
// Wait for all results
2026-01-21 22:13:15 -05:00
const [data1, data2] = await Promise.all([p1, p2]);
2025-12-03 10:57:05 -08:00
```
### Cache expensive operations
2026-01-21 22:13:15 -05:00
Store results between invocations to avoid repeated computation, especially for
hooks that run frequently (like `BeforeTool` or `AfterModel` ).
2025-12-03 10:57:05 -08:00
```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]) {
2026-01-21 22:13:15 -05:00
// Write JSON to stdout
2025-12-03 10:57:05 -08:00
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.
2026-01-21 22:13:15 -05:00
- **`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.
2025-12-03 10:57:05 -08:00
### Filter with matchers
Use specific matchers to avoid unnecessary hook execution. Instead of matching
2026-01-21 22:13:15 -05:00
all tools with `*` , specify only the tools you need. This saves the overhead of
spawning a process for irrelevant events.
2025-12-03 10:57:05 -08:00
```json
{
2025-12-17 02:51:47 +05:00
"matcher": "write_file|replace",
2025-12-03 10:57:05 -08:00
"hooks": [
{
"name": "validate-writes",
2026-01-25 18:33:12 -05:00
"type": "command",
2025-12-03 10:57:05 -08:00
"command": "./validate.sh"
}
]
}
```
### Optimize JSON parsing
2026-01-21 22:13:15 -05:00
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.
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
## Debugging
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
### The "Strict JSON" rule
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
The most common cause of hook failure is "polluting" the standard output.
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
- **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"}'
```
2025-12-03 10:57:05 -08:00
### Log to files
2026-01-21 22:13:15 -05:00
Since hooks run in the background, writing to a dedicated log file is often the
easiest way to debug complex logic.
2025-12-03 10:57:05 -08:00
```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"
2026-01-21 22:13:15 -05:00
# Always output valid JSON to stdout at the end, even if just empty
echo "{}"
2025-12-03 10:57:05 -08:00
```
### 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) {
2026-01-21 22:13:15 -05:00
// Write the error description to stderr so the user/agent sees it
2025-12-03 10:57:05 -08:00
console.error(`Hook error: ${error.message}` );
process.exit(2); // Blocking error
}
```
### Test hooks independently
2026-01-21 22:13:15 -05:00
Run hook scripts manually with sample JSON input to verify they behave as
expected before hooking them up to the CLI.
2025-12-03 10:57:05 -08:00
2026-02-27 15:41:47 -08:00
**macOS/Linux**
2025-12-03 10:57:05 -08:00
```bash
# Create test input
cat > test-input.json << 'EOF'
{
"session_id": "test-123",
"cwd": "/tmp/test",
"hook_event_name": "BeforeTool",
2025-12-17 02:51:47 +05:00
"tool_name": "write_file",
2025-12-03 10:57:05 -08:00
"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: $?"
2026-02-27 15:41:47 -08:00
```
**Windows (PowerShell)**
2026-01-21 22:13:15 -05:00
2026-02-27 15:41:47 -08:00
```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"
2025-12-03 10:57:05 -08:00
```
### Check exit codes
2026-01-21 22:13:15 -05:00
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.
2025-12-03 10:57:05 -08:00
```bash
#!/usr/bin/env bash
2026-01-21 22:13:15 -05:00
set -e
2025-12-03 10:57:05 -08:00
# Hook logic
if process_input; then
2026-01-21 22:13:15 -05:00
echo '{"decision": "allow"}'
2025-12-03 10:57:05 -08:00
exit 0
else
2026-01-21 22:13:15 -05:00
echo "Critical validation failure" >&2
2025-12-03 10:57:05 -08:00
exit 2
fi
2026-01-21 22:13:15 -05:00
2025-12-03 10:57:05 -08:00
```
### Enable telemetry
2026-01-21 22:13:15 -05:00
Hook execution is logged when `telemetry.logPrompts` is enabled. You can view
these logs to debug execution flow.
2025-12-03 10:57:05 -08:00
```json
{
"telemetry": {
"logPrompts": true
}
}
```
### Use hook panel
2026-01-21 22:13:15 -05:00
The `/hooks panel` command inside the CLI shows execution status and recent
output:
2025-12-03 10:57:05 -08:00
```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
2026-01-21 22:13:15 -05:00
# 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
*/
2025-12-03 10:57:05 -08:00
```
### Use JSON libraries
2026-01-21 22:13:15 -05:00
Parse JSON with proper libraries instead of text processing.
2025-12-03 10:57:05 -08:00
**Bad:**
```bash
# Fragile text parsing
tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')
2026-01-21 22:13:15 -05:00
2025-12-03 10:57:05 -08:00
```
**Good:**
```bash
# Robust JSON parsing
tool_name=$(echo "$input" | jq -r '.tool_name')
2026-01-21 22:13:15 -05:00
2025-12-03 10:57:05 -08:00
```
### Make scripts executable
2026-02-27 15:41:47 -08:00
Always make hook scripts executable on macOS/Linux:
2025-12-03 10:57:05 -08:00
```bash
chmod +x .gemini/hooks/*.sh
chmod +x .gemini/hooks/*.js
2026-01-21 22:13:15 -05:00
2025-12-03 10:57:05 -08:00
```
2026-02-27 15:41:47 -08:00
**Windows Note**: On Windows, PowerShell scripts (`.ps1` ) don't use `chmod` , but
you may need to ensure your execution policy allows them to run (e.g.,
`Set-ExecutionPolicy RemoteSigned -Scope CurrentUser` ).
2025-12-03 10:57:05 -08:00
### Version control
Commit hooks to share with your team:
```bash
git add .gemini/hooks/
git add .gemini/settings.json
2026-01-21 22:13:15 -05:00
2025-12-03 10:57:05 -08:00
```
**`.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
2026-01-21 22:13:15 -05:00
2025-12-03 10:57:05 -08:00
```
2026-01-21 22:13:15 -05:00
## Hook security
### Threat Model
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
Understanding where hooks come from and what they can do is critical for secure
usage.
| Hook Source | Description |
| :---------------------------- | :------------------------------------------------------------------------------------------------------------------------- |
| **System ** | Configured by system administrators (e.g., `/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 (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
| 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 (e.g., verified
publishers, well-known community members).
- Be cautious with obfuscated scripts or compiled binaries from unknown sources.
#### Sanitize environment
Hooks inherit the environment of the Gemini CLI process, which may include
sensitive API keys. Gemini CLI provides a
2026-03-09 08:23:00 -07:00
[redaction system ](../reference/configuration.md#environment-variable-redaction )
2026-01-21 22:13:15 -05:00
that automatically filters variables matching sensitive patterns (e.g., `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` .
2025-12-03 10:57:05 -08:00
```json
{
2026-01-21 22:13:15 -05:00
"security": {
"environmentVariableRedaction": {
"enabled": true,
"allowed": ["MY_REQUIRED_TOOL_KEY"]
}
2025-12-03 10:57:05 -08:00
}
}
```
2026-01-21 22:13:15 -05:00
**System administrators:** You can enforce redaction for all users in the system
configuration.
2025-12-03 10:57:05 -08:00
## Troubleshooting
### Hook not executing
2026-01-21 22:13:15 -05:00
**Check hook name in `/hooks panel` :** Verify the hook appears in the list and
is enabled.
2025-12-03 10:57:05 -08:00
**Verify matcher pattern:**
```bash
# Test regex pattern
2025-12-17 02:51:47 +05:00
echo "write_file|replace" | grep -E "write_.*|replace"
2026-01-21 22:13:15 -05:00
2025-12-03 10:57:05 -08:00
```
2026-01-21 22:13:15 -05:00
**Check disabled list:** Verify the hook is not listed in your `settings.json` :
2025-12-03 10:57:05 -08:00
```json
{
"hooks": {
"disabled": ["my-hook-name"]
}
}
```
2026-01-21 22:13:15 -05:00
**Ensure script is executable**: For macOS and Linux users, verify the script
has execution permissions:
2025-12-03 10:57:05 -08:00
```bash
ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh
```
2026-02-27 15:41:47 -08:00
**Windows Note**: On Windows, ensure your execution policy allows running
scripts (e.g., `Get-ExecutionPolicy` ).
2026-01-21 22:13:15 -05:00
**Verify script path:** Ensure the path in `settings.json` resolves correctly.
2025-12-03 10:57:05 -08:00
```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
2026-01-21 22:13:15 -05:00
**Check configured timeout:** The default is 60000ms (1 minute). You can
increase this in `settings.json` :
2025-12-03 10:57:05 -08:00
```json
{
"name": "slow-hook",
2026-01-21 22:13:15 -05:00
"timeout": 120000
2025-12-03 10:57:05 -08:00
}
```
2026-01-21 22:13:15 -05:00
**Optimize slow operations:** Move heavy processing to background tasks or use
caching.
2025-12-03 10:57:05 -08:00
### 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
2026-01-02 15:22:30 -05:00
```
2026-01-21 22:13:15 -05:00
## Authoring secure hooks
2026-01-02 15:22:30 -05:00
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",
2026-01-25 18:33:12 -05:00
"type": "command",
2026-01-02 15:22:30 -05:00
"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));
}
```
2025-12-03 10:57:05 -08:00
## Privacy considerations
2026-01-21 22:13:15 -05:00
Hook inputs and outputs may contain sensitive information.
2025-12-03 10:57:05 -08:00
### What data is collected
2026-01-21 22:13:15 -05:00
Hook telemetry may include inputs (prompts, code) and outputs (decisions,
reasons) unless disabled.
2025-12-03 10:57:05 -08:00
### Privacy settings
2026-01-21 22:13:15 -05:00
**Disable PII logging:** If you are working with sensitive data, disable prompt
logging in your settings:
2025-12-03 10:57:05 -08:00
```json
{
"telemetry": {
"logPrompts": false
}
}
```
2026-01-21 22:13:15 -05:00
**Suppress Output:** Individual hooks can request their metadata be hidden from
logs and telemetry by returning `"suppressOutput": true` in their JSON response.
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
> **Note**
> `suppressOutput` only affects background logging. Any `systemMessage` or
> `reason` included in the JSON will still be displayed to the user in the
> terminal.
2025-12-03 10:57:05 -08:00
### Sensitive data in hooks
If your hooks process sensitive data:
2026-01-21 22:13:15 -05:00
1. **Minimize logging: ** Don't write sensitive data to log files.
2. **Sanitize outputs: ** Remove sensitive data before outputting JSON or writing
to stderr.