|
Download docs/hooks/best-practices.md from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 17.7 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/docs/hooks/best-practices.md
- Command line
-
hf download hf://SaylorTwift/gemini-cli/docs/hooks/best-practices.md
-
curl -L -o best-practices.md https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/docs/hooks/best-practices.md
17.7 kB
| # 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. | |