2025-12-03 10:57:05 -08:00
# Writing hooks for Gemini CLI
This guide will walk you through creating hooks for Gemini CLI, from a simple
2026-01-21 22:13:15 -05:00
logging hook to a comprehensive workflow assistant.
2025-12-03 10:57:05 -08:00
## 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.
2026-01-21 22:13:15 -05:00
**Crucial Rule: ** Always write logs to `stderr` . Write only the final JSON to
`stdout` .
2025-12-03 10:57:05 -08:00
### Step 1: Create your hook script
2026-01-21 22:13:15 -05:00
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.
2025-12-03 10:57:05 -08:00
``` bash
mkdir -p .gemini/hooks
cat > .gemini/hooks/log-tools.sh << 'EOF'
#!/usr/bin/env bash
# Read hook input from stdin
input=$(cat)
2026-01-21 22:13:15 -05:00
# Extract tool name (requires jq)
2025-12-03 10:57:05 -08:00
tool_name=$(echo "$input" | jq -r '.tool_name')
2026-01-21 22:13:15 -05:00
# Log to stderr (visible in terminal if hook fails, or captured in logs)
echo "Logging tool: $tool_name" >&2
2025-12-03 10:57:05 -08:00
# Log to file
echo "[$(date)] Tool executed: $tool_name" >> .gemini/tool-log.txt
2026-01-21 22:13:15 -05:00
# Return success (exit 0) with empty JSON
echo "{}"
exit 0
2025-12-03 10:57:05 -08:00
EOF
chmod +x .gemini/hooks/log-tools.sh
```
2026-01-21 22:13:15 -05:00
## Exit Code Strategies
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
There are two ways to control or block an action in Gemini CLI:
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
| 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. |
2025-12-03 10:57:05 -08:00
## Practical examples
### Security: Block secrets in commits
2026-01-21 22:13:15 -05:00
Prevent committing files containing API keys or passwords. Note that we use
**Exit Code 0 ** to provide a structured denial message to the agent.
2025-12-03 10:57:05 -08:00
* * `.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
2026-01-21 22:13:15 -05:00
# Log to stderr
echo "Blocked potential secret" >& 2
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
# Return structured denial to stdout
cat <<EOF
2025-12-03 10:57:05 -08:00
{
2026-01-21 22:13:15 -05:00
"decision": "deny",
"reason": "Security Policy: Potential secret detected in content.",
"systemMessage": "🔒 Security scanner blocked operation"
2025-12-03 10:57:05 -08:00
}
2026-01-21 22:13:15 -05:00
EOF
2025-12-03 10:57:05 -08:00
exit 0
fi
2026-01-21 22:13:15 -05:00
# Allow
echo '{"decision": "allow"}'
2025-12-03 10:57:05 -08:00
exit 0
```
2026-01-21 22:13:15 -05:00
### Dynamic context injection (Git History)
2025-12-03 10:57:05 -08:00
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
```
2026-01-21 22:13:15 -05:00
### RAG-based Tool Filtering (BeforeToolSelection)
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
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.
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
* * `.gemini/hooks/filter-tools.js` :**
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
``` javascript
#!/usr/bin/env node
const fs = require ( 'fs' ) ;
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
async function main ( ) {
const input = JSON . parse ( fs . readFileSync ( 0 , 'utf-8' ) ) ;
const { llm _request } = input ;
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
// Decoupled API: Access messages from llm_request
const messages = llm _request . messages || [ ] ;
const lastUserMessage = messages
. slice ( )
. reverse ( )
. find ( ( m ) => m . role === 'user' ) ;
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
if ( ! lastUserMessage ) {
console . log ( JSON . stringify ( { } ) ) ; // Do nothing
return ;
}
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
const text = lastUserMessage . content ;
const allowed = [ 'write_todos' ] ; // Always allow memory
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
// 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' ) ;
}
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
// 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 ( { } ) ) ;
}
}
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
main ( ) . catch ( ( err ) => {
console . error ( err ) ;
process . exit ( 1 ) ;
} ) ;
```
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
* * `.gemini/settings.json` :**
2025-12-03 10:57:05 -08:00
``` json
{
"hooks" : {
2026-01-21 22:13:15 -05:00
"BeforeToolSelection" : [
2025-12-03 10:57:05 -08:00
{
"matcher" : "*" ,
"hooks" : [
{
2026-01-21 22:13:15 -05:00
"name" : "intent-filter" ,
2026-01-25 18:33:12 -05:00
"type" : "command" ,
2026-01-21 22:13:15 -05:00
"command" : "node .gemini/hooks/filter-tools.js"
2025-12-03 10:57:05 -08:00
}
]
}
]
}
}
```
2026-01-21 22:13:15 -05:00
> **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.
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
## Complete example: Smart Development Workflow Assistant
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
This comprehensive example demonstrates all hook events working together. We
will build a system that maintains memory, filters tools, and checks for
security.
2025-12-03 10:57:05 -08:00
### Architecture
2026-01-21 22:13:15 -05:00
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.
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
### Configuration (`.gemini/settings.json`)
2025-12-03 10:57:05 -08:00
``` json
{
"hooks" : {
"SessionStart" : [
{
"matcher" : "startup" ,
2026-01-25 18:33:12 -05:00
"hooks" : [
{
"name" : "init" ,
"type" : "command" ,
"command" : "node .gemini/hooks/init.js"
}
]
2025-12-03 10:57:05 -08:00
}
] ,
"BeforeAgent" : [
{
"matcher" : "*" ,
"hooks" : [
{
2026-01-21 22:13:15 -05:00
"name" : "memory" ,
2026-01-25 18:33:12 -05:00
"type" : "command" ,
2026-01-21 22:13:15 -05:00
"command" : "node .gemini/hooks/inject-memories.js"
2025-12-03 10:57:05 -08:00
}
]
}
] ,
"BeforeToolSelection" : [
{
"matcher" : "*" ,
"hooks" : [
2026-01-25 18:33:12 -05:00
{
"name" : "filter" ,
"type" : "command" ,
"command" : "node .gemini/hooks/rag-filter.js"
}
2025-12-03 10:57:05 -08:00
]
}
] ,
"BeforeTool" : [
{
2026-01-21 22:13:15 -05:00
"matcher" : "write_file" ,
2025-12-03 10:57:05 -08:00
"hooks" : [
2026-01-25 18:33:12 -05:00
{
"name" : "security" ,
"type" : "command" ,
"command" : "node .gemini/hooks/security.js"
}
2025-12-03 10:57:05 -08:00
]
}
] ,
2026-01-21 22:13:15 -05:00
"AfterModel" : [
2025-12-03 10:57:05 -08:00
{
2026-01-21 22:13:15 -05:00
"matcher" : "*" ,
2025-12-03 10:57:05 -08:00
"hooks" : [
2026-01-25 18:33:12 -05:00
{
"name" : "record" ,
"type" : "command" ,
"command" : "node .gemini/hooks/record.js"
}
2025-12-03 10:57:05 -08:00
]
}
] ,
2026-01-21 22:13:15 -05:00
"AfterAgent" : [
2025-12-03 10:57:05 -08:00
{
"matcher" : "*" ,
"hooks" : [
2026-01-25 18:33:12 -05:00
{
"name" : "validate" ,
"type" : "command" ,
"command" : "node .gemini/hooks/validate.js"
}
2025-12-03 10:57:05 -08:00
]
}
] ,
"SessionEnd" : [
{
2026-01-21 22:13:15 -05:00
"matcher" : "exit" ,
2025-12-03 10:57:05 -08:00
"hooks" : [
2026-01-25 18:33:12 -05:00
{
"name" : "save" ,
"type" : "command" ,
"command" : "node .gemini/hooks/consolidate.js"
}
2025-12-03 10:57:05 -08:00
]
}
]
}
}
```
2026-01-21 22:13:15 -05:00
### Hook Scripts
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
> **Note**: For brevity, these scripts use `console.error` for logging and
> standard `console.log` for JSON output.
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
#### 1. Initialize (`init.js`)
2025-12-03 10:57:05 -08:00
``` javascript
#!/usr/bin/env node
2026-01-21 22:13:15 -05:00
// Initialize DB or resources
console . error ( 'Initializing assistant...' ) ;
// Output to user
console . log (
JSON . stringify ( {
systemMessage : '🧠 Smart Assistant Loaded' ,
} ) ,
) ;
2025-12-03 10:57:05 -08:00
```
2026-01-21 22:13:15 -05:00
#### 2. Inject Memories (`inject-memories.js`)
2025-12-03 10:57:05 -08:00
``` javascript
#!/usr/bin/env node
2026-01-21 22:13:15 -05:00
const fs = require ( 'fs' ) ;
2025-12-03 10:57:05 -08:00
async function main ( ) {
2026-01-21 22:13:15 -05:00
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.' ;
2025-12-03 10:57:05 -08:00
console . log (
JSON . stringify ( {
hookSpecificOutput : {
2026-01-21 22:13:15 -05:00
hookEventName : 'BeforeAgent' ,
additionalContext : ` \n ## Relevant Memories \n ${ memories } ` ,
2025-12-03 10:57:05 -08:00
} ,
} ) ,
) ;
}
2026-01-21 22:13:15 -05:00
main ( ) ;
2025-12-03 10:57:05 -08:00
```
2026-01-21 22:13:15 -05:00
#### 3. Security Check (`security.js`)
2025-12-03 10:57:05 -08:00
``` javascript
#!/usr/bin/env node
const fs = require ( 'fs' ) ;
2026-01-21 22:13:15 -05:00
const input = JSON . parse ( fs . readFileSync ( 0 ) ) ;
const content = input . tool _input . content || '' ;
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
if ( content . includes ( 'SECRET_KEY' ) ) {
console . log (
JSON . stringify ( {
decision : 'deny' ,
reason : 'Found SECRET_KEY in content' ,
systemMessage : '🚨 Blocked sensitive commit' ,
} ) ,
) ;
process . exit ( 0 ) ;
2025-12-03 10:57:05 -08:00
}
2026-01-21 22:13:15 -05:00
console . log ( JSON . stringify ( { decision : 'allow' } ) ) ;
2025-12-03 10:57:05 -08:00
```
2026-01-21 22:13:15 -05:00
#### 4. Record Interaction (`record.js`)
2025-12-03 10:57:05 -08:00
``` javascript
#!/usr/bin/env node
const fs = require ( 'fs' ) ;
const path = require ( 'path' ) ;
2026-01-21 22:13:15 -05:00
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 ( { } ) ) ;
2025-12-03 10:57:05 -08:00
```
2026-01-21 22:13:15 -05:00
#### 5. Validate Response (`validate.js`)
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
``` javascript
2025-12-03 10:57:05 -08:00
#!/usr/bin/env node
const fs = require ( 'fs' ) ;
2026-01-21 22:13:15 -05:00
const input = JSON . parse ( fs . readFileSync ( 0 ) ) ;
const response = input . prompt _response ;
2025-12-03 10:57:05 -08:00
2026-01-21 22:13:15 -05:00
// 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...' ,
} ) ,
2025-12-03 10:57:05 -08:00
) ;
2026-01-21 22:13:15 -05:00
process . exit ( 0 ) ;
2025-12-03 10:57:05 -08:00
}
2026-01-21 22:13:15 -05:00
console . log ( JSON . stringify ( { decision : 'allow' } ) ) ;
2025-12-03 10:57:05 -08:00
```
2026-01-21 22:13:15 -05:00
#### 6. Consolidate Memories (`consolidate.js`)
2025-12-03 10:57:05 -08:00
``` javascript
2026-01-21 22:13:15 -05:00
#!/usr/bin/env node
// Logic to save final session state
console . error ( 'Consolidating memories for session end...' ) ;
2025-12-03 10:57:05 -08:00
```
2026-01-07 15:46:44 -05:00
## Packaging as an extension
2026-01-21 22:13:15 -05:00
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 ](https://www.google.com/search?q=../extensions/index.md ).
This provides version control, easy distribution, and centralized management.