|
| 1 | +# Hooks |
| 2 | + |
| 3 | +Hooks allow you to execute custom shell scripts at specific points in the Gemini |
| 4 | +CLI lifecycle. This enables powerful customizations like audit logging, context |
| 5 | +injection, tool filtering, and integration with external systems. |
| 6 | + |
| 7 | +## Quick Start |
| 8 | + |
| 9 | +### 1. Enable Hooks |
| 10 | + |
| 11 | +In `~/.gemini/settings.json`: |
| 12 | + |
| 13 | +```json |
| 14 | +{ |
| 15 | + "tools": { |
| 16 | + "enableHooks": true |
| 17 | + } |
| 18 | +} |
| 19 | +``` |
| 20 | + |
| 21 | +### 2. Configure a Hook |
| 22 | + |
| 23 | +```json |
| 24 | +{ |
| 25 | + "hooks": { |
| 26 | + "SessionStart": [ |
| 27 | + { |
| 28 | + "hooks": [ |
| 29 | + { |
| 30 | + "type": "command", |
| 31 | + "command": "~/.gemini/hooks/my-hook.sh", |
| 32 | + "timeout": 10000 |
| 33 | + } |
| 34 | + ] |
| 35 | + } |
| 36 | + ] |
| 37 | + } |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +### 3. Create the Hook Script |
| 42 | + |
| 43 | +```bash |
| 44 | +#!/bin/bash |
| 45 | +# ~/.gemini/hooks/my-hook.sh |
| 46 | + |
| 47 | +# Read JSON input from stdin |
| 48 | +INPUT=$(cat) |
| 49 | + |
| 50 | +# Parse event name |
| 51 | +EVENT=$(echo "$INPUT" | jq -r '.hook_event_name') |
| 52 | + |
| 53 | +# Log to file |
| 54 | +echo "[$(date)] Event: $EVENT" >> /tmp/gemini_hooks.log |
| 55 | + |
| 56 | +# Output valid JSON |
| 57 | +echo '{"continue": true}' |
| 58 | +``` |
| 59 | + |
| 60 | +Make it executable: |
| 61 | + |
| 62 | +```bash |
| 63 | +chmod +x ~/.gemini/hooks/my-hook.sh |
| 64 | +``` |
| 65 | + |
| 66 | +## Hook Events |
| 67 | + |
| 68 | +| Event | When It Fires | Key Input Fields | |
| 69 | +| --------------------- | --------------------------------- | ------------------------------------------ | |
| 70 | +| `SessionStart` | CLI starts up, resumes, or clears | `source` | |
| 71 | +| `SessionEnd` | CLI exits | `reason` | |
| 72 | +| `BeforeAgent` | Before processing user prompt | `prompt` | |
| 73 | +| `AfterAgent` | After agent responds | `prompt`, `prompt_response` | |
| 74 | +| `BeforeTool` | Before a tool executes | `tool_name`, `tool_input` | |
| 75 | +| `AfterTool` | After a tool completes | `tool_name`, `tool_input`, `tool_response` | |
| 76 | +| `PreCompress` | Before context compression | `trigger` | |
| 77 | +| `BeforeModel` | Before LLM API call | `llm_request` | |
| 78 | +| `AfterModel` | After LLM API response | `llm_request`, `llm_response` | |
| 79 | +| `BeforeToolSelection` | Before tool selection | `llm_request` | |
| 80 | + |
| 81 | +## Hook Input Format |
| 82 | + |
| 83 | +All hooks receive JSON via stdin with these common fields: |
| 84 | + |
| 85 | +```json |
| 86 | +{ |
| 87 | + "session_id": "abc123...", |
| 88 | + "transcript_path": "/path/to/session.json", |
| 89 | + "cwd": "/current/working/directory", |
| 90 | + "hook_event_name": "BeforeAgent", |
| 91 | + "timestamp": "2025-01-15T10:30:00.000Z" |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +Plus event-specific fields (see table above). |
| 96 | + |
| 97 | +## Hook Output Format |
| 98 | + |
| 99 | +Hooks should output JSON to stdout: |
| 100 | + |
| 101 | +```json |
| 102 | +{ |
| 103 | + "continue": true, |
| 104 | + "decision": "allow", |
| 105 | + "reason": "Optional reason text", |
| 106 | + "systemMessage": "Message to display to user" |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +### Exit Codes |
| 111 | + |
| 112 | +| Code | Meaning | |
| 113 | +| ---- | -------------------------- | |
| 114 | +| `0` | Success | |
| 115 | +| `1` | Warning (non-blocking) | |
| 116 | +| `2` | Blocking error (deny/stop) | |
| 117 | + |
| 118 | +## Configuration Structure |
| 119 | + |
| 120 | +```json |
| 121 | +{ |
| 122 | + "hooks": { |
| 123 | + "[EventName]": [ |
| 124 | + { |
| 125 | + "hooks": [ |
| 126 | + { |
| 127 | + "type": "command", |
| 128 | + "command": "path/to/script.sh", |
| 129 | + "timeout": 60000, |
| 130 | + "enabled": true |
| 131 | + } |
| 132 | + ], |
| 133 | + "matcher": "pattern", |
| 134 | + "sequential": false |
| 135 | + } |
| 136 | + ] |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +### Fields |
| 142 | + |
| 143 | +| Field | Type | Description | |
| 144 | +| ------------ | ------- | ---------------------------------------- | |
| 145 | +| `type` | string | Always `"command"` | |
| 146 | +| `command` | string | Path to script or shell command | |
| 147 | +| `timeout` | number | Timeout in milliseconds (default: 60000) | |
| 148 | +| `enabled` | boolean | Enable/disable this hook (default: true) | |
| 149 | +| `matcher` | string | Pattern for tool-related hooks (regex) | |
| 150 | +| `sequential` | boolean | Run hooks sequentially vs parallel | |
| 151 | + |
| 152 | +## Tool Name Reference |
| 153 | + |
| 154 | +For `BeforeTool` and `AfterTool` hooks, use these tool names in matchers: |
| 155 | + |
| 156 | +| Tool | Name in Gemini CLI | |
| 157 | +| ----------------- | --------------------- | |
| 158 | +| File editing | `replace` | |
| 159 | +| File writing | `write_file` | |
| 160 | +| File reading | `read_file` | |
| 161 | +| Shell commands | `run_shell_command` | |
| 162 | +| Todo lists | `write_todos` | |
| 163 | +| File globbing | `glob` | |
| 164 | +| Content search | `search_file_content` | |
| 165 | +| Directory listing | `list_directory` | |
| 166 | +| Web fetch | `web_fetch` | |
| 167 | +| Web search | `google_web_search` | |
| 168 | + |
| 169 | +### Matcher Examples |
| 170 | + |
| 171 | +```json |
| 172 | +{ |
| 173 | + "AfterTool": [ |
| 174 | + { |
| 175 | + "hooks": [ |
| 176 | + { "type": "command", "command": "~/.gemini/hooks/log-edits.sh" } |
| 177 | + ], |
| 178 | + "matcher": "replace|write_file" |
| 179 | + }, |
| 180 | + { |
| 181 | + "hooks": [ |
| 182 | + { "type": "command", "command": "~/.gemini/hooks/log-shell.sh" } |
| 183 | + ], |
| 184 | + "matcher": "run_shell_command" |
| 185 | + } |
| 186 | + ] |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +## Migrating from Claude Code |
| 191 | + |
| 192 | +### Automatic Migration |
| 193 | + |
| 194 | +```bash |
| 195 | +gemini hooks migrate |
| 196 | +``` |
| 197 | + |
| 198 | +This command: |
| 199 | + |
| 200 | +1. Reads your `~/.claude/settings.json` |
| 201 | +2. Transforms event names and tool matchers |
| 202 | +3. Converts timeouts (seconds → milliseconds) |
| 203 | +4. Updates script paths (`.claude/hooks` → `.gemini/hooks`) |
| 204 | +5. Saves to `~/.gemini/settings.json` |
| 205 | + |
| 206 | +### Event Name Mapping |
| 207 | + |
| 208 | +| Claude Code | Gemini CLI | |
| 209 | +| ------------------ | -------------- | |
| 210 | +| `UserPromptSubmit` | `BeforeAgent` | |
| 211 | +| `PostToolUse` | `AfterTool` | |
| 212 | +| `Stop` | `AfterAgent` | |
| 213 | +| `PreCompact` | `PreCompress` | |
| 214 | +| `SessionStart` | `SessionStart` | |
| 215 | +| `SessionEnd` | `SessionEnd` | |
| 216 | + |
| 217 | +### Tool Name Mapping |
| 218 | + |
| 219 | +| Claude Code | Gemini CLI | |
| 220 | +| ----------- | --------------------- | |
| 221 | +| `Edit` | `replace` | |
| 222 | +| `MultiEdit` | `replace` | |
| 223 | +| `Write` | `write_file` | |
| 224 | +| `Read` | `read_file` | |
| 225 | +| `Bash` | `run_shell_command` | |
| 226 | +| `TodoWrite` | `write_todos` | |
| 227 | +| `Glob` | `glob` | |
| 228 | +| `Grep` | `search_file_content` | |
| 229 | + |
| 230 | +### Field Name Differences |
| 231 | + |
| 232 | +| Concept | Claude Code | Gemini CLI | |
| 233 | +| ---------- | ----------- | --------------------- | |
| 234 | +| User input | `.message` | `.prompt` | |
| 235 | +| Event name | N/A | `.hook_event_name` | |
| 236 | +| Tool name | Same | Different (see table) | |
| 237 | + |
| 238 | +### Compatibility Shim |
| 239 | + |
| 240 | +For existing Claude Code scripts, use the compatibility shim: |
| 241 | + |
| 242 | +```bash |
| 243 | +#!/bin/bash |
| 244 | +source ~/.gemini/hooks/utils/claude-compat-shim.sh |
| 245 | + |
| 246 | +# Now use normalized variables: |
| 247 | +echo "User message: $USER_MESSAGE" |
| 248 | +echo "Session ID: $SESSION_ID" |
| 249 | +echo "Event: $HOOK_EVENT_NAME" |
| 250 | +echo "Tool (normalized): $TOOL_NAME_NORMALIZED" |
| 251 | +``` |
| 252 | + |
| 253 | +The shim provides: |
| 254 | + |
| 255 | +- `$USER_MESSAGE` - Works with both `.message` (Claude) and `.prompt` (Gemini) |
| 256 | +- `$TOOL_NAME_NORMALIZED` - Maps Gemini tool names to Claude-style names |
| 257 | +- Helper functions: `hook_success()`, `hook_block()`, `hook_warn()` |
| 258 | + |
| 259 | +## Examples |
| 260 | + |
| 261 | +### Audit Logger |
| 262 | + |
| 263 | +Log all tool executions: |
| 264 | + |
| 265 | +```bash |
| 266 | +#!/bin/bash |
| 267 | +# ~/.gemini/hooks/audit-logger.sh |
| 268 | + |
| 269 | +INPUT=$(cat) |
| 270 | +EVENT=$(echo "$INPUT" | jq -r '.hook_event_name') |
| 271 | +TOOL=$(echo "$INPUT" | jq -r '.tool_name // "N/A"') |
| 272 | + |
| 273 | +echo "[$(date)] $EVENT - Tool: $TOOL" >> /tmp/gemini_audit.log |
| 274 | +echo '{"continue": true}' |
| 275 | +``` |
| 276 | + |
| 277 | +### Context Injector |
| 278 | + |
| 279 | +Add project context to prompts: |
| 280 | + |
| 281 | +```bash |
| 282 | +#!/bin/bash |
| 283 | +# ~/.gemini/hooks/inject-context.sh |
| 284 | + |
| 285 | +INPUT=$(cat) |
| 286 | + |
| 287 | +# Read project context |
| 288 | +if [ -f ".gemini/context.md" ]; then |
| 289 | + CONTEXT=$(cat .gemini/context.md) |
| 290 | + echo "{\"continue\": true, \"hookSpecificOutput\": {\"additionalContext\": \"$CONTEXT\"}}" |
| 291 | +else |
| 292 | + echo '{"continue": true}' |
| 293 | +fi |
| 294 | +``` |
| 295 | + |
| 296 | +### Tool Guardian |
| 297 | + |
| 298 | +Block dangerous commands: |
| 299 | + |
| 300 | +```bash |
| 301 | +#!/bin/bash |
| 302 | +# ~/.gemini/hooks/tool-guard.sh |
| 303 | + |
| 304 | +INPUT=$(cat) |
| 305 | +TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input.command // ""') |
| 306 | + |
| 307 | +# Block rm -rf commands |
| 308 | +if echo "$TOOL_INPUT" | grep -q 'rm.*-rf'; then |
| 309 | + echo '{"decision": "block", "reason": "Dangerous rm -rf command blocked"}' |
| 310 | + exit 2 |
| 311 | +fi |
| 312 | + |
| 313 | +echo '{"continue": true}' |
| 314 | +``` |
| 315 | + |
| 316 | +## Hook Commands |
| 317 | + |
| 318 | +Manage hooks with these CLI commands: |
| 319 | + |
| 320 | +```bash |
| 321 | +# List all configured hooks |
| 322 | +gemini hooks list |
| 323 | + |
| 324 | +# Disable a hook |
| 325 | +gemini hooks disable "~/.gemini/hooks/my-hook.sh" |
| 326 | + |
| 327 | +# Enable a disabled hook |
| 328 | +gemini hooks enable "~/.gemini/hooks/my-hook.sh" |
| 329 | + |
| 330 | +# Migrate from Claude Code |
| 331 | +gemini hooks migrate |
| 332 | +``` |
| 333 | + |
| 334 | +## Debugging |
| 335 | + |
| 336 | +### Test Your Hook |
| 337 | + |
| 338 | +```bash |
| 339 | +# Manually test with sample input |
| 340 | +echo '{"hook_event_name": "BeforeAgent", "prompt": "test", "session_id": "123", "cwd": "/tmp", "timestamp": "2025-01-01T00:00:00Z"}' | ~/.gemini/hooks/my-hook.sh |
| 341 | +``` |
| 342 | + |
| 343 | +### View Hook Execution |
| 344 | + |
| 345 | +Check the debug log for hook execution details: |
| 346 | + |
| 347 | +```bash |
| 348 | +# Enable debug mode |
| 349 | +export DEBUG=gemini:* |
| 350 | + |
| 351 | +# Or check hook output in your script |
| 352 | +tail -f /tmp/gemini_hooks.log |
| 353 | +``` |
| 354 | + |
| 355 | +### Common Issues |
| 356 | + |
| 357 | +1. **Hook not firing**: Ensure `tools.enableHooks: true` in settings |
| 358 | +2. **"UnknownEvent" logged**: Read event name from JSON stdin, not command args |
| 359 | +3. **Permission denied**: Make script executable with `chmod +x` |
| 360 | +4. **Timeout errors**: Increase `timeout` value or optimize script |
| 361 | +5. **Matcher not working**: Use Gemini tool names (e.g., `replace` not `Edit`) |
| 362 | + |
| 363 | +## Environment Variables |
| 364 | + |
| 365 | +Hooks have access to these environment variables: |
| 366 | + |
| 367 | +| Variable | Description | |
| 368 | +| ----------------------------------------- | ------------------------- | |
| 369 | +| `GEMINI_PROJECT_DIR` | Current working directory | |
| 370 | +| `CLAUDE_PROJECT_DIR` | Same (for compatibility) | |
| 371 | +| Plus all existing `process.env` variables | |
0 commit comments