# 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 (e.g., `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 (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 [redaction system](../reference/configuration.md#environment-variable-redaction) 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`. ```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 (e.g., `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.