Docs track: Current (v0.1). Versioned docs planned.

Hooks on TerminaI: Best practices

This guide covers security considerations, performance optimization, debugging techniques, and privacy considerations for developing and deploying hooks in TerminaI.

Security considerations

Validate all inputs

Never trust data from hooks without validation. Hook inputs may contain user-provided data that could be malicious:

#!/usr/bin/env bashinput=$(cat)
# Validate JSON structureif ! echo "$input" | jq empty 2>/dev/null; then  echo "Invalid JSON input" >&2  exit 1fi
# Validate required fieldstool_name=$(echo "$input" | jq -r '.tool_name // empty')if [ -z "$tool_name" ]; then  echo "Missing tool_name field" >&2  exit 1fi

Use timeouts

Set reasonable timeouts to prevent hooks from hanging indefinitely:

{  "hooks": {    "BeforeTool": [      {        "matcher": "*",        "hooks": [          {            "name": "slow-validator",            "type": "command",            "command": "./hooks/validate.sh",            "timeout": 5000          }        ]      }    ]  }}

Recommended timeouts:

  • Fast validation: 1000-5000ms
  • Network requests: 10000-30000ms
  • Heavy computation: 30000-60000ms

Limit permissions

Run hooks with minimal required permissions:

#!/usr/bin/env bash# Don't run as rootif [ "$EUID" -eq 0 ]; then  echo "Hook should not run as root" >&2  exit 1fi
# Check file permissions before writingif [ -w "$file_path" ]; then  # Safe to writeelse  echo "Insufficient permissions" >&2  exit 1fi

Scan for secrets

Use BeforeTool hooks to prevent committing sensitive data:

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));}

Review external scripts

Always review hook scripts from untrusted sources before enabling them:

Terminal window

# Review before installingcat third-party-hook.sh | less
# Check for suspicious patternsgrep -E 'curl|wget|ssh|eval' third-party-hook.sh
# Verify hook sourcels -la third-party-hook.sh

Sandbox untrusted hooks

For maximum security, consider running untrusted hooks in isolated environments:

Terminal window

# Run hook in Docker containerdocker run --rm \  -v "$GEMINI_PROJECT_DIR:/workspace:ro" \  -i untrusted-hook-image \  /hook-script.sh < input.json

Performance

Keep hooks fast

Hooks run synchronously—slow hooks delay the agent loop. Optimize for speed by using parallel operations:

// Sequential operations are slowerconst data1 = await fetch(url1).then((r) => r.json());const data2 = await fetch(url2).then((r) => r.json());const data3 = await fetch(url3).then((r) => r.json());
// Prefer parallel operations for better performanceconst [data1, data2, data3] = await Promise.all([  fetch(url1).then((r) => r.json()),  fetch(url2).then((r) => r.json()),  fetch(url3).then((r) => r.json()),]);

Cache expensive operations

Store results between invocations to avoid repeated computation:

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]) {    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 agent loop completion, while AfterModel fires after every LLM call (potentially multiple times per loop):

// If checking final completion, use AfterAgent instead of AfterModel{  "hooks": {    "AfterAgent": [      {        "matcher": "*",        "hooks": [          {            "name": "final-checker",            "command": "./check-completion.sh"          }        ]      }    ]  }}

Filter with matchers

Use specific matchers to avoid unnecessary hook execution. Instead of matching all tools with *, specify only the tools you need:

{  "matcher": "write_file|replace",  "hooks": [    {      "name": "validate-writes",      "command": "./validate.sh"    }  ]}

Optimize JSON parsing

For large inputs, use streaming JSON parsers to avoid loading everything into memory:

// Standard approach: parse entire inputconst input = JSON.parse(await readStdin());const content = input.tool_input.content;
// For very large inputs: stream and extract only needed fieldsconst { createReadStream } = require('fs');const JSONStream = require('JSONStream');
const stream = createReadStream(0).pipe(JSONStream.parse('tool_input.content'));let content = '';stream.on('data', (chunk) => {  content += chunk;});

Debugging

Log to files

Write debug information to dedicated log files:

#!/usr/bin/env bashLOG_FILE=".gemini/hooks/debug.log"
# Log with timestamplog() {  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"

Use stderr for errors

Error messages on stderr are surfaced appropriately based on exit codes:

try {  const result = dangerousOperation();  console.log(JSON.stringify({ result }));} catch (error) {  console.error(`Hook error: ${error.message}`);  process.exit(2); // Blocking error}

Test hooks independently

Run hook scripts manually with sample JSON input:

Terminal window

# Create test inputcat > 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 hookcat test-input.json | .gemini/hooks/my-hook.sh
# Check exit codeecho "Exit code: $?"

Check exit codes

Ensure your script returns the correct exit code:

#!/usr/bin/env bashset -e  # Exit on error
# Hook logicprocess_input() {  # ...}
if process_input; then  echo "Success message"  exit 0else  echo "Error message" >&2  exit 2fi

Enable telemetry

Hook execution is logged when telemetry.logPrompts is enabled:

{  "telemetry": {    "logPrompts": true  }}

View hook telemetry in logs to debug execution issues.

Use hook panel

The /hooks panel command shows execution status and recent output:

Terminal window

/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:

#!/usr/bin/env bash# Simple logging hook to understand input structureinput=$(cat)echo "$input" >> .gemini/hook-inputs.logecho "Logged input"

Use JSON libraries

Parse JSON with proper libraries instead of text processing:

Bad:

Terminal window

# Fragile text parsingtool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')

Good:

Terminal window

# Robust JSON parsingtool_name=$(echo "$input" | jq -r '.tool_name')

Make scripts executable

Always make hook scripts executable:

Terminal window

chmod +x .gemini/hooks/*.shchmod +x .gemini/hooks/*.js

Version control

Commit hooks to share with your team:

Terminal window

git add .gemini/hooks/git add .gemini/settings.jsongit commit -m "Add project hooks for security and testing"

.gitignore considerations:

# 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

Document behavior

Add descriptions to help others understand your hooks:

{  "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, passwords, and other secrets before writing"          }        ]      }    ]  }}

Add comments in hook scripts:

#!/usr/bin/env node/** * RAG Tool Filter Hook * * This hook reduces the tool space from 100+ tools to ~15 relevant ones * by extracting keywords from the user's request and filtering tools * based on semantic similarity. * * Performance: ~500ms average, cached tool embeddings * Dependencies: @google/generative-ai */

Troubleshooting

Hook not executing

Check hook name in /hooks panel:

Terminal window

/hooks panel

Verify the hook appears in the list and is enabled.

Verify matcher pattern:

Terminal window

# Test regex patternecho "write_file|replace" | grep -E "write_.*|replace"

Check disabled list:

{  "hooks": {    "disabled": ["my-hook-name"]  }}

Ensure script is executable:

Terminal window

ls -la .gemini/hooks/my-hook.shchmod +x .gemini/hooks/my-hook.sh

Verify script path:

Terminal window

# Check path expansionecho "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# Verify file existstest -f "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh" && echo "File exists"

Hook timing out

Check configured timeout:

{  "name": "slow-hook",  "timeout": 60000}

Optimize slow operations:

// Before: Sequential operations (slow)for (const item of items) {  await processItem(item);}
// After: Parallel operations (fast)await Promise.all(items.map((item) => processItem(item)));

Use caching:

const cache = new Map();
async function getCachedData(key) {  if (cache.has(key)) {    return cache.get(key);  }  const data = await fetchData(key);  cache.set(key, data);  return data;}

Consider splitting into multiple faster hooks:

{  "hooks": {    "BeforeTool": [      {        "matcher": "write_file",        "hooks": [          {            "name": "quick-check",            "command": "./quick-validation.sh",            "timeout": 1000          }        ]      },      {        "matcher": "write_file",        "hooks": [          {            "name": "deep-check",            "command": "./deep-analysis.sh",            "timeout": 30000          }        ]      }    ]  }}

Invalid JSON output

Validate JSON before outputting:

#!/usr/bin/env bashoutput='{"decision": "allow"}'
# Validate JSONif echo "$output" | jq empty 2>/dev/null; then  echo "$output"else  echo "Invalid JSON generated" >&2  exit 1fi

Ensure proper quoting and escaping:

// Bad: Unescaped string interpolationconst message = `User said: ${userInput}`;console.log(JSON.stringify({ message }));
// Good: Automatic escapingconsole.log(JSON.stringify({ message: `User said: ${userInput}` }));

Check for binary data or control characters:

function sanitizeForJSON(str) {  return str.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); // Remove control chars}
const cleanContent = sanitizeForJSON(content);console.log(JSON.stringify({ content: cleanContent }));

Exit code issues

Verify script returns correct codes:

#!/usr/bin/env bashset -e  # Exit on error
# Processing logicif validate_input; then  echo "Success"  exit 0else  echo "Validation failed" >&2  exit 2fi

Check for unintended errors:

#!/usr/bin/env bash# Don't use 'set -e' if you want to handle errors explicitly# set -e
if ! command_that_might_fail; then  # Handle error  echo "Command failed but continuing" >&2fi
# Always exit explicitlyexit 0

Use trap for cleanup:

#!/usr/bin/env bash
cleanup() {  # Cleanup logic  rm -f /tmp/hook-temp-*}
trap cleanup EXIT
# Hook logic here

Environment variables not available

Check if variable is set:

#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then  echo "GEMINI_PROJECT_DIR not set" >&2  exit 1fi
if [ -z "$CUSTOM_VAR" ]; then  echo "Warning: CUSTOM_VAR not set, using default" >&2  CUSTOM_VAR="default-value"fi

Debug available variables:

#!/usr/bin/env bash
# List all environment variablesenv > .gemini/hook-env.log
# Check specific variablesecho "GEMINI_PROJECT_DIR: $GEMINI_PROJECT_DIR" >> .gemini/hook-env.logecho "GEMINI_SESSION_ID: $GEMINI_SESSION_ID" >> .gemini/hook-env.logecho "GEMINI_API_KEY: ${GEMINI_API_KEY:+<set>}" >> .gemini/hook-env.log

Use .env files:

#!/usr/bin/env bash
# Load .env file if it existsif [ -f "$GEMINI_PROJECT_DIR/.env" ]; then  source "$GEMINI_PROJECT_DIR/.env"fi

Privacy considerations

Hook inputs and outputs may contain sensitive information. TerminaI respects the telemetry.logPrompts setting for hook data logging.

What data is collected

Hook telemetry may include:

  • Hook inputs: User prompts, tool arguments, file contents
  • Hook outputs: Hook responses, decision reasons, added context
  • Standard streams: stdout and stderr from hook processes
  • Execution metadata: Hook name, event type, duration, success/failure

Privacy settings

Enabled (default):

Full hook I/O is logged to telemetry. Use this when:

  • Developing and debugging hooks
  • Telemetry is redirected to a trusted enterprise system
  • You understand and accept the privacy implications

Disabled:

Only metadata is logged (event name, duration, success/failure). Hook inputs and outputs are excluded. Use this when:

  • Sending telemetry to third-party systems
  • Working with sensitive data
  • Privacy regulations require minimizing data collection

Configuration

Disable PII logging in settings:

{  "telemetry": {    "logPrompts": false  }}

Disable via environment variable:

Terminal window

export GEMINI_TELEMETRY_LOG_PROMPTS=false

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
  3. Use secure storage: Encrypt sensitive data at rest
  4. Limit access: Restrict hook script permissions

Example sanitization:

function sanitizeOutput(data) {  const sanitized = { ...data };
  // Remove sensitive fields  delete sanitized.apiKey;  delete sanitized.password;
  // Redact sensitive strings  if (sanitized.content) {    sanitized.content = sanitized.content.replace(      /api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/gi,      '[REDACTED]',    );  }
  return sanitized;}
console.log(JSON.stringify(sanitizeOutput(hookOutput)));

Learn more