|
Download docs/hooks/writing-hooks.md from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/docs/hooks/writing-hooks.md
- Command line
-
hf download hf://SaylorTwift/gemini-cli/docs/hooks/writing-hooks.md
-
curl -L -o writing-hooks.md https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/docs/hooks/writing-hooks.md
11.5 kB
| # Writing hooks for Gemini CLI | |
| This guide will walk you through creating hooks for Gemini CLI, from a simple | |
| logging hook to a comprehensive workflow assistant. | |
| ## Prerequisites | |
| Before you start, make sure you have: | |
| - Gemini CLI installed and configured | |
| - Basic understanding of shell scripting or JavaScript/Node.js | |
| - Familiarity with JSON for hook input/output | |
| ## Quick start | |
| Let's create a simple hook that logs all tool executions to understand the | |
| basics. | |
| **Crucial Rule:** Always write logs to `stderr`. Write only the final JSON to | |
| `stdout`. | |
| ### Step 1: Create your hook script | |
| Create a directory for hooks and a simple logging script. | |
| > **Note**: | |
| > | |
| > This example uses `jq` to parse JSON. If you don't have it installed, you can | |
| > perform similar logic using Node.js or Python. | |
| **macOS/Linux** | |
| ```bash | |
| mkdir -p .gemini/hooks | |
| cat > .gemini/hooks/log-tools.sh << 'EOF' | |
| #!/usr/bin/env bash | |
| # Read hook input from stdin | |
| input=$(cat) | |
| # Extract tool name (requires jq) | |
| tool_name=$(echo "$input" | jq -r '.tool_name') | |
| # Log to stderr (visible in terminal if hook fails, or captured in logs) | |
| echo "Logging tool: $tool_name" >&2 | |
| # Log to file | |
| echo "[$(date)] Tool executed: $tool_name" >> .gemini/tool-log.txt | |
| # Return success (exit 0) with empty JSON | |
| echo "{}" | |
| exit 0 | |
| EOF | |
| chmod +x .gemini/hooks/log-tools.sh | |
| ``` | |
| **Windows (PowerShell)** | |
| ```powershell | |
| New-Item -ItemType Directory -Force -Path ".gemini\hooks" | |
| @" | |
| # Read hook input from stdin | |
| `$inputJson = `$input | Out-String | ConvertFrom-Json | |
| # Extract tool name | |
| `$toolName = `$inputJson.tool_name | |
| # Log to stderr (visible in terminal if hook fails, or captured in logs) | |
| [Console]::Error.WriteLine("Logging tool: `$toolName") | |
| # Log to file | |
| "[`$(Get-Date -Format 'o')] Tool executed: `$toolName" | Out-File -FilePath ".gemini\tool-log.txt" -Append -Encoding utf8 | |
| # Return success with empty JSON | |
| "{}" | |
| "@ | Out-File -FilePath ".gemini\hooks\log-tools.ps1" -Encoding utf8 | |
| ``` | |
| ## Exit Code Strategies | |
| There are two ways to control or block an action in Gemini CLI: | |
| | Strategy | Exit Code | Implementation | Best For | | |
| | :------------------------- | :-------- | :----------------------------------------------------------------- | :---------------------------------------------------------- | | |
| | **Structured (Idiomatic)** | `0` | Return a JSON object like `{"decision": "deny", "reason": "..."}`. | Production hooks, custom user feedback, and complex logic. | | |
| | **Emergency Brake** | `2` | Print the error message to `stderr` and exit. | Simple security gates, script errors, or rapid prototyping. | | |
| ## Practical examples | |
| ### Security: Block secrets in commits | |
| Prevent committing files containing API keys or passwords. Note that we use | |
| **Exit Code 0** to provide a structured denial message to the agent. | |
| **`.gemini/hooks/block-secrets.sh`:** | |
| ```bash | |
| #!/usr/bin/env bash | |
| input=$(cat) | |
| # Extract content being written | |
| content=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // ""') | |
| # Check for secrets | |
| if echo "$content" | grep -qE 'api[_-]?key|password|secret'; then | |
| # Log to stderr | |
| echo "Blocked potential secret" >&2 | |
| # Return structured denial to stdout | |
| cat <<EOF | |
| { | |
| "decision": "deny", | |
| "reason": "Security Policy: Potential secret detected in content.", | |
| "systemMessage": "🔒 Security scanner blocked operation" | |
| } | |
| EOF | |
| exit 0 | |
| fi | |
| # Allow | |
| echo '{"decision": "allow"}' | |
| exit 0 | |
| ``` | |
| ### Dynamic context injection (Git History) | |
| Add relevant project context before each agent interaction. | |
| **`.gemini/hooks/inject-context.sh`:** | |
| ```bash | |
| #!/usr/bin/env bash | |
| # Get recent git commits for context | |
| context=$(git log -5 --oneline 2>/dev/null || echo "No git history") | |
| # Return as JSON | |
| cat <<EOF | |
| { | |
| "hookSpecificOutput": { | |
| "hookEventName": "BeforeAgent", | |
| "additionalContext": "Recent commits:\n$context" | |
| } | |
| } | |
| EOF | |
| ``` | |
| ### RAG-based Tool Filtering (BeforeToolSelection) | |
| Use `BeforeToolSelection` to intelligently reduce the tool space. This example | |
| uses a Node.js script to check the user's prompt and allow only relevant tools. | |
| **`.gemini/hooks/filter-tools.js`:** | |
| ```javascript | |
| #!/usr/bin/env node | |
| const fs = require('fs'); | |
| async function main() { | |
| const input = JSON.parse(fs.readFileSync(0, 'utf-8')); | |
| const { llm_request } = input; | |
| // Decoupled API: Access messages from llm_request | |
| const messages = llm_request.messages || []; | |
| const lastUserMessage = messages | |
| .slice() | |
| .reverse() | |
| .find((m) => m.role === 'user'); | |
| if (!lastUserMessage) { | |
| console.log(JSON.stringify({})); // Do nothing | |
| return; | |
| } | |
| const text = lastUserMessage.content; | |
| const allowed = ['write_todos']; // Always allow memory | |
| // Simple keyword matching | |
| if (text.includes('read') || text.includes('check')) { | |
| allowed.push('read_file', 'list_directory'); | |
| } | |
| if (text.includes('test')) { | |
| allowed.push('run_shell_command'); | |
| } | |
| // If we found specific intent, filter tools. Otherwise allow all. | |
| if (allowed.length > 1) { | |
| console.log( | |
| JSON.stringify({ | |
| hookSpecificOutput: { | |
| hookEventName: 'BeforeToolSelection', | |
| toolConfig: { | |
| mode: 'ANY', // Force usage of one of these tools (or AUTO) | |
| allowedFunctionNames: allowed, | |
| }, | |
| }, | |
| }), | |
| ); | |
| } else { | |
| console.log(JSON.stringify({})); | |
| } | |
| } | |
| main().catch((err) => { | |
| console.error(err); | |
| process.exit(1); | |
| }); | |
| ``` | |
| **`.gemini/settings.json`:** | |
| ```json | |
| { | |
| "hooks": { | |
| "BeforeToolSelection": [ | |
| { | |
| "matcher": "*", | |
| "hooks": [ | |
| { | |
| "name": "intent-filter", | |
| "type": "command", | |
| "command": "node .gemini/hooks/filter-tools.js" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| ``` | |
| > **TIP** | |
| > | |
| > **Union Aggregation Strategy**: `BeforeToolSelection` is unique in that it | |
| > combines the results of all matching hooks. If you have multiple filtering | |
| > hooks, the agent will receive the **union** of all whitelisted tools. Only | |
| > using `mode: "NONE"` will override other hooks to disable all tools. | |
| ## Complete example: Smart Development Workflow Assistant | |
| This comprehensive example demonstrates all hook events working together. We | |
| will build a system that maintains memory, filters tools, and checks for | |
| security. | |
| ### Architecture | |
| 1. **SessionStart**: Load project memories. | |
| 2. **BeforeAgent**: Inject memories into context. | |
| 3. **BeforeToolSelection**: Filter tools based on intent. | |
| 4. **BeforeTool**: Scan for secrets. | |
| 5. **AfterModel**: Record interactions. | |
| 6. **AfterAgent**: Validate final response quality (Retry). | |
| 7. **SessionEnd**: Consolidate memories. | |
| ### Configuration (`.gemini/settings.json`) | |
| ```json | |
| { | |
| "hooks": { | |
| "SessionStart": [ | |
| { | |
| "matcher": "startup", | |
| "hooks": [ | |
| { | |
| "name": "init", | |
| "type": "command", | |
| "command": "node .gemini/hooks/init.js" | |
| } | |
| ] | |
| } | |
| ], | |
| "BeforeAgent": [ | |
| { | |
| "matcher": "*", | |
| "hooks": [ | |
| { | |
| "name": "memory", | |
| "type": "command", | |
| "command": "node .gemini/hooks/inject-memories.js" | |
| } | |
| ] | |
| } | |
| ], | |
| "BeforeToolSelection": [ | |
| { | |
| "matcher": "*", | |
| "hooks": [ | |
| { | |
| "name": "filter", | |
| "type": "command", | |
| "command": "node .gemini/hooks/rag-filter.js" | |
| } | |
| ] | |
| } | |
| ], | |
| "BeforeTool": [ | |
| { | |
| "matcher": "write_file", | |
| "hooks": [ | |
| { | |
| "name": "security", | |
| "type": "command", | |
| "command": "node .gemini/hooks/security.js" | |
| } | |
| ] | |
| } | |
| ], | |
| "AfterModel": [ | |
| { | |
| "matcher": "*", | |
| "hooks": [ | |
| { | |
| "name": "record", | |
| "type": "command", | |
| "command": "node .gemini/hooks/record.js" | |
| } | |
| ] | |
| } | |
| ], | |
| "AfterAgent": [ | |
| { | |
| "matcher": "*", | |
| "hooks": [ | |
| { | |
| "name": "validate", | |
| "type": "command", | |
| "command": "node .gemini/hooks/validate.js" | |
| } | |
| ] | |
| } | |
| ], | |
| "SessionEnd": [ | |
| { | |
| "matcher": "exit", | |
| "hooks": [ | |
| { | |
| "name": "save", | |
| "type": "command", | |
| "command": "node .gemini/hooks/consolidate.js" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| ``` | |
| ### Hook Scripts | |
| > **Note**: For brevity, these scripts use `console.error` for logging and | |
| > standard `console.log` for JSON output. | |
| #### 1. Initialize (`init.js`) | |
| ```javascript | |
| #!/usr/bin/env node | |
| // Initialize DB or resources | |
| console.error('Initializing assistant...'); | |
| // Output to user | |
| console.log( | |
| JSON.stringify({ | |
| systemMessage: '🧠 Smart Assistant Loaded', | |
| }), | |
| ); | |
| ``` | |
| #### 2. Inject Memories (`inject-memories.js`) | |
| ```javascript | |
| #!/usr/bin/env node | |
| const fs = require('fs'); | |
| async function main() { | |
| const input = JSON.parse(fs.readFileSync(0, 'utf-8')); | |
| // Assume we fetch memories from a DB here | |
| const memories = '- [Memory] Always use TypeScript for this project.'; | |
| console.log( | |
| JSON.stringify({ | |
| hookSpecificOutput: { | |
| hookEventName: 'BeforeAgent', | |
| additionalContext: `\n## Relevant Memories\n${memories}`, | |
| }, | |
| }), | |
| ); | |
| } | |
| main(); | |
| ``` | |
| #### 3. Security Check (`security.js`) | |
| ```javascript | |
| #!/usr/bin/env node | |
| const fs = require('fs'); | |
| const input = JSON.parse(fs.readFileSync(0)); | |
| const content = input.tool_input.content || ''; | |
| if (content.includes('SECRET_KEY')) { | |
| console.log( | |
| JSON.stringify({ | |
| decision: 'deny', | |
| reason: 'Found SECRET_KEY in content', | |
| systemMessage: '🚨 Blocked sensitive commit', | |
| }), | |
| ); | |
| process.exit(0); | |
| } | |
| console.log(JSON.stringify({ decision: 'allow' })); | |
| ``` | |
| #### 4. Record Interaction (`record.js`) | |
| ```javascript | |
| #!/usr/bin/env node | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const input = JSON.parse(fs.readFileSync(0)); | |
| const { llm_request, llm_response } = input; | |
| const logFile = path.join( | |
| process.env.GEMINI_PROJECT_DIR, | |
| '.gemini/memory/session.jsonl', | |
| ); | |
| fs.appendFileSync( | |
| logFile, | |
| JSON.stringify({ | |
| request: llm_request, | |
| response: llm_response, | |
| timestamp: new Date().toISOString(), | |
| }) + '\n', | |
| ); | |
| console.log(JSON.stringify({})); | |
| ``` | |
| #### 5. Validate Response (`validate.js`) | |
| ```javascript | |
| #!/usr/bin/env node | |
| const fs = require('fs'); | |
| const input = JSON.parse(fs.readFileSync(0)); | |
| const response = input.prompt_response; | |
| // Example: Check if the agent forgot to include a summary | |
| if (!response.includes('Summary:')) { | |
| console.log( | |
| JSON.stringify({ | |
| decision: 'block', // Triggers an automatic retry turn | |
| reason: 'Your response is missing a Summary section. Please add one.', | |
| systemMessage: '🔄 Requesting missing summary...', | |
| }), | |
| ); | |
| process.exit(0); | |
| } | |
| console.log(JSON.stringify({ decision: 'allow' })); | |
| ``` | |
| #### 6. Consolidate Memories (`consolidate.js`) | |
| ```javascript | |
| #!/usr/bin/env node | |
| // Logic to save final session state | |
| console.error('Consolidating memories for session end...'); | |
| ``` | |
| ## Packaging as an extension | |
| While project-level hooks are great for specific repositories, you can share | |
| your hooks across multiple projects by packaging them as a | |
| [Gemini CLI extension](../extensions/index.md). This provides version control, | |
| easy distribution, and centralized management. | |