mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 22:55:13 +00:00
feat(hooks): Hook System Documentation (#14307)
This commit is contained in:
806
docs/hooks/best-practices.md
Normal file
806
docs/hooks/best-practices.md
Normal file
@@ -0,0 +1,806 @@
|
|||||||
|
# Hooks on Gemini CLI: Best practices
|
||||||
|
|
||||||
|
This guide covers security considerations, performance optimization, debugging
|
||||||
|
techniques, and privacy considerations for developing and deploying hooks in
|
||||||
|
Gemini CLI.
|
||||||
|
|
||||||
|
## Security considerations
|
||||||
|
|
||||||
|
### Validate all inputs
|
||||||
|
|
||||||
|
Never trust data from hooks without validation. Hook inputs may contain
|
||||||
|
user-provided data that could be malicious:
|
||||||
|
|
||||||
|
```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 required fields
|
||||||
|
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
||||||
|
if [ -z "$tool_name" ]; then
|
||||||
|
echo "Missing tool_name field" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use timeouts
|
||||||
|
|
||||||
|
Set reasonable timeouts to prevent hooks from hanging indefinitely:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"BeforeTool": [
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"name": "slow-validator",
|
||||||
|
"type": "command",
|
||||||
|
"command": "./hooks/validate.sh",
|
||||||
|
"timeout": 5000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended timeouts:**
|
||||||
|
|
||||||
|
- Fast validation: 1000-5000ms
|
||||||
|
- Network requests: 10000-30000ms
|
||||||
|
- Heavy computation: 30000-60000ms
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scan for secrets
|
||||||
|
|
||||||
|
Use `BeforeTool` hooks to prevent committing sensitive data:
|
||||||
|
|
||||||
|
```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));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review external scripts
|
||||||
|
|
||||||
|
Always review hook scripts from untrusted sources before enabling them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Review before installing
|
||||||
|
cat third-party-hook.sh | less
|
||||||
|
|
||||||
|
# Check for suspicious patterns
|
||||||
|
grep -E 'curl|wget|ssh|eval' third-party-hook.sh
|
||||||
|
|
||||||
|
# Verify hook source
|
||||||
|
ls -la third-party-hook.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sandbox untrusted hooks
|
||||||
|
|
||||||
|
For maximum security, consider running untrusted hooks in isolated environments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run hook in Docker container
|
||||||
|
docker run --rm \
|
||||||
|
-v "$GEMINI_PROJECT_DIR:/workspace:ro" \
|
||||||
|
-i untrusted-hook-image \
|
||||||
|
/hook-script.sh < input.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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());
|
||||||
|
const data3 = await fetch(url3).then((r) => r.json());
|
||||||
|
|
||||||
|
// Prefer parallel operations for better performance
|
||||||
|
const [data1, data2, data3] = await Promise.all([
|
||||||
|
fetch(url1).then((r) => r.json()),
|
||||||
|
fetch(url2).then((r) => r.json()),
|
||||||
|
fetch(url3).then((r) => r.json()),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache expensive operations
|
||||||
|
|
||||||
|
Store results between invocations to avoid repeated computation:
|
||||||
|
|
||||||
|
```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]) {
|
||||||
|
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 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter with matchers
|
||||||
|
|
||||||
|
Use specific matchers to avoid unnecessary hook execution. Instead of matching
|
||||||
|
all tools with `*`, specify only the tools you need:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"matcher": "WriteFile|Edit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"name": "validate-writes",
|
||||||
|
"command": "./validate.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### Log to files
|
||||||
|
|
||||||
|
Write debug information to dedicated log files:
|
||||||
|
|
||||||
|
```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"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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) {
|
||||||
|
console.error(`Hook error: ${error.message}`);
|
||||||
|
process.exit(2); // Blocking error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test hooks independently
|
||||||
|
|
||||||
|
Run hook scripts manually with sample JSON input:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create test input
|
||||||
|
cat > test-input.json << 'EOF'
|
||||||
|
{
|
||||||
|
"session_id": "test-123",
|
||||||
|
"cwd": "/tmp/test",
|
||||||
|
"hook_event_name": "BeforeTool",
|
||||||
|
"tool_name": "WriteFile",
|
||||||
|
"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: $?"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check exit codes
|
||||||
|
|
||||||
|
Ensure your script returns the correct exit code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Hook logic
|
||||||
|
process_input() {
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
|
||||||
|
if process_input; then
|
||||||
|
echo "Success message"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Error message" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable telemetry
|
||||||
|
|
||||||
|
Hook execution is logged when `telemetry.logPrompts` is enabled:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"logPrompts": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
View hook telemetry in logs to debug execution issues.
|
||||||
|
|
||||||
|
### Use hook panel
|
||||||
|
|
||||||
|
The `/hooks panel` command 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
|
||||||
|
echo "Logged input"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x .gemini/hooks/*.sh
|
||||||
|
chmod +x .gemini/hooks/*.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version control
|
||||||
|
|
||||||
|
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:**
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Document behavior
|
||||||
|
|
||||||
|
Add descriptions to help others understand your hooks:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"BeforeTool": [
|
||||||
|
{
|
||||||
|
"matcher": "WriteFile|Edit",
|
||||||
|
"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 "WriteFile" | grep -E "Write.*|Edit"
|
||||||
|
```
|
||||||
|
|
||||||
|
**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": "WriteFile",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"name": "quick-check",
|
||||||
|
"command": "./quick-validation.sh",
|
||||||
|
"timeout": 1000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "WriteFile",
|
||||||
|
"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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Privacy considerations
|
||||||
|
|
||||||
|
Hook inputs and outputs may contain sensitive information. Gemini CLI respects
|
||||||
|
the `telemetry.logPrompts` setting for hook data logging.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### 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:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"logPrompts": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Disable via environment variable:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GEMINI_TELEMETRY_LOG_PROMPTS=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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](../cli/configuration.md) - Gemini CLI settings
|
||||||
|
- [Hooks Design Document](../hooks-design.md) - Technical architecture
|
||||||
560
docs/hooks/index.md
Normal file
560
docs/hooks/index.md
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
See [writing hooks guide](writing-hooks.md) for a tutorial on creating your
|
||||||
|
first hook and a comprehensive example.
|
||||||
|
|
||||||
|
See [best practices](best-practices.md) for 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.
|
||||||
|
|
||||||
|
## Core concepts
|
||||||
|
|
||||||
|
### Hook events
|
||||||
|
|
||||||
|
Hooks are triggered by specific events in Gemini CLI's lifecycle. The following
|
||||||
|
table lists all available hook events:
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
### Hook types
|
||||||
|
|
||||||
|
Gemini CLI currently supports **command hooks** that run shell commands or
|
||||||
|
scripts:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh",
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Plugin hooks (npm packages) are planned for a future release.
|
||||||
|
|
||||||
|
### Matchers
|
||||||
|
|
||||||
|
For tool-related events (`BeforeTool`, `AfterTool`), you can filter which tools
|
||||||
|
trigger the hook:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"BeforeTool": [
|
||||||
|
{
|
||||||
|
"matcher": "WriteFile|Edit",
|
||||||
|
"hooks": [
|
||||||
|
/* hooks for write operations */
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Matcher patterns:**
|
||||||
|
|
||||||
|
- **Exact match:** `"ReadFile"` matches only `ReadFile`
|
||||||
|
- **Regex:** `"Write.*|Edit"` matches `WriteFile`, `WriteBinary`, `Edit`
|
||||||
|
- **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",
|
||||||
|
"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": "WriteFile",
|
||||||
|
"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": "ReadFile",
|
||||||
|
"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": ["ReadFile", "WriteFile"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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": ["ReadFile", "WriteFile", "Edit"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or simple output (comma-separated tool names sets mode to ANY):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "ReadFile,WriteFile,Edit"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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 precedence (higher
|
||||||
|
numbers override lower numbers):
|
||||||
|
|
||||||
|
1. **System defaults:** Built-in default settings (lowest precedence)
|
||||||
|
2. **User settings:** `~/.gemini/settings.json`
|
||||||
|
3. **Project settings:** `.gemini/settings.json` in your project directory
|
||||||
|
4. **System settings:** `/etc/gemini-cli/settings.json` (highest precedence)
|
||||||
|
|
||||||
|
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",
|
||||||
|
"type": "command",
|
||||||
|
"command": "./path/to/script.sh",
|
||||||
|
"description": "What this hook does",
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration properties:**
|
||||||
|
|
||||||
|
- **`name`** (string, required): Unique identifier for the hook used in
|
||||||
|
`/hooks enable/disable` commands
|
||||||
|
- **`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:
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
## Managing hooks
|
||||||
|
|
||||||
|
### View registered hooks
|
||||||
|
|
||||||
|
Use the `/hooks panel` command to view all registered hooks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/hooks panel
|
||||||
|
```
|
||||||
|
|
||||||
|
This command displays:
|
||||||
|
|
||||||
|
- All active hooks organized by event
|
||||||
|
- Hook source (user, project, system)
|
||||||
|
- Hook type (command or plugin)
|
||||||
|
- Execution status and recent output
|
||||||
|
|
||||||
|
### Enable and disable hooks
|
||||||
|
|
||||||
|
You can temporarily 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.
|
||||||
|
|
||||||
|
### 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` → `RunShellCommand`, `Edit` → `Edit`)
|
||||||
|
- 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` | `RunShellCommand` |
|
||||||
|
| `Edit` | `Edit` |
|
||||||
|
| `Read` | `ReadFile` |
|
||||||
|
| `Write` | `WriteFile` |
|
||||||
|
|
||||||
|
## 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](../cli/configuration.md) - Gemini CLI configuration options
|
||||||
|
- [Hooks Design Document](../hooks-design.md) - Technical architecture details
|
||||||
1026
docs/hooks/writing-hooks.md
Normal file
1026
docs/hooks/writing-hooks.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -102,6 +102,15 @@ This documentation is organized into the following sections:
|
|||||||
- **[Extension releasing](./extensions/extension-releasing.md):** How to release
|
- **[Extension releasing](./extensions/extension-releasing.md):** How to release
|
||||||
Gemini CLI extensions.
|
Gemini CLI extensions.
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
- **[Hooks](./hooks/index.md):** Intercept and customize Gemini CLI behavior at
|
||||||
|
key lifecycle points.
|
||||||
|
- **[Writing Hooks](./hooks/writing-hooks.md):** Learn how to create your first
|
||||||
|
hook with a comprehensive example.
|
||||||
|
- **[Best Practices](./hooks/best-practices.md):** Security, performance, and
|
||||||
|
debugging guidelines for hooks.
|
||||||
|
|
||||||
### IDE integration
|
### IDE integration
|
||||||
|
|
||||||
- **[Introduction to IDE integration](./ide-integration/index.md):** Connect the
|
- **[Introduction to IDE integration](./ide-integration/index.md):** Connect the
|
||||||
|
|||||||
@@ -193,6 +193,23 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Hooks",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"label": "Introduction",
|
||||||
|
"slug": "docs/hooks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Writing hooks",
|
||||||
|
"slug": "docs/hooks/writing-hooks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Best practices",
|
||||||
|
"slug": "docs/hooks/best-practices"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "IDE integration",
|
"label": "IDE integration",
|
||||||
"items": [
|
"items": [
|
||||||
|
|||||||
Reference in New Issue
Block a user