Skip to main content

Hooks System

Hooks are lifecycle event handlers that execute at specific points in the platform’s lifecycle, allowing Superpowers to inject context, run setup scripts, or perform initialization tasks.

What Are Hooks?

Hooks are automatic scripts that run when certain events occur in your AI coding platform. They enable:
  • Context Injection: Adding information to the agent’s context at session start
  • Setup Tasks: Running initialization scripts before work begins
  • Environment Configuration: Setting up project-specific requirements
  • Custom Workflows: Triggering platform-specific automation
Hooks are transparent to users - they run automatically without user intervention.

Hook Configuration

Hooks are defined in hooks/hooks.json:
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|clear|compact",
        "hooks": [
          {
            "type": "command",
            "command": "'${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd' session-start",
            "async": false
          }
        ]
      }
    ]
  }
}

Configuration Anatomy

Different platforms support different hook events:
  • SessionStart: Runs when a new session begins (supported by Claude Code, Cursor)
  • SessionResume: Runs when resuming a saved session
  • ProjectOpen: Runs when opening a project
  • Custom events: Platform-specific hooks
Currently, Superpowers only implements SessionStart.
The matcher field filters when hooks run:
"matcher": "startup|resume|clear|compact"
This regex matches session states:
  • startup: New session
  • resume: Resumed session
  • clear: Cleared session
  • compact: Compacted session
The hook runs if the session state matches any of these patterns.
Each hook has:
  • type: Command type (usually "command")
  • command: Shell command to execute
  • async: Whether to run asynchronously (false = blocking)
Environment variables available:
  • ${CLAUDE_PLUGIN_ROOT}: Path to plugin root directory
  • ${CURSOR_PLUGIN_ROOT}: Path to plugin root (Cursor)
  • Standard shell variables ($HOME, $USER, etc.)

SessionStart Hook

The SessionStart hook is the most important hook in Superpowers - it injects the using-superpowers skill content into every session.

Hook Script: hooks/session-start

#!/usr/bin/env bash
# SessionStart hook for superpowers plugin

set -euo pipefail

# Determine plugin root directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"

# Check if legacy skills directory exists and build warning
warning_message=""
legacy_skills_dir="${HOME}/.config/superpowers/skills"
if [ -d "$legacy_skills_dir" ]; then
    warning_message="\n\n<important-reminder>⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead.</important-reminder>"
fi

# Read using-superpowers content
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")

# Escape string for JSON embedding
escape_for_json() {
    local s="$1"
    s="${s//\\/\\\\}"  # Backslashes
    s="${s//\"/\\\"}"  # Quotes
    s="${s//$'\n'/\\n}"  # Newlines
    s="${s//$'\r'/\\r}"  # Carriage returns
    s="${s//$'\t'/\\t}"  # Tabs
    printf '%s' "$s"
}

using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
warning_escaped=$(escape_for_json "$warning_message")
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"

# Output context injection as JSON
cat <<EOF
{
  "additional_context": "${session_context}",
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "${session_context}"
  }
}
EOF

exit 0

What the Hook Does

1

Determine Plugin Root

Finds the plugin installation directory using SCRIPT_DIR:
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
2

Check for Legacy Configuration

Detects old installation paths and warns users:
if [ -d "$legacy_skills_dir" ]; then
    warning_message="⚠️ WARNING: Move skills to new location"
fi
3

Read using-superpowers Skill

Loads the core skill that teaches agents how to use skills:
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md")
4

Escape for JSON

Properly escapes content for JSON embedding (critical for special characters):
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
Performance note: Uses bash parameter substitution instead of character-by-character loops for 10-100x speedup.
5

Output JSON Context

Returns JSON with dual format for compatibility:
{
  "additional_context": "...",
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "..."
  }
}
  • Cursor: Uses additional_context
  • Claude Code: Uses hookSpecificOutput.additionalContext

Cross-Platform Hook Wrapper

The hooks/run-hook.cmd script is a polyglot (runs on both Windows and Unix):
: << 'CMDBLOCK'
@echo off
REM Windows batch script portion
if "%~1"=="" (
    echo run-hook.cmd: missing script name >&2
    exit /b 1
)

set "HOOK_DIR=%~dp0"

# Try Git for Windows bash
if exist "C:\Program Files\Git\bin\bash.exe" (
    "C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1"
    exit /b %ERRORLEVEL%
)

# Try bash on PATH
where bash >nul 2>nul
if %ERRORLEVEL% equ 0 (
    bash "%HOOK_DIR%%~1"
    exit /b %ERRORLEVEL%
)

# No bash found - exit silently
exit /b 0
CMDBLOCK

# Unix portion
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"

How the Polyglot Works

When cmd.exe runs the script:
  1. The : << 'CMDBLOCK' is ignored (: is a no-op in batch)
  2. Batch portion executes normally
  3. Searches for Git Bash in standard locations
  4. Falls back to bash on PATH
  5. If no bash found, exits silently (plugin still works without hooks)
When bash/sh runs the script:
  1. The : << 'CMDBLOCK' starts a heredoc that ignores the batch portion
  2. CMDBLOCK closes the heredoc
  3. Unix portion executes normally
  4. Directly calls bash with the hook script
Why extensionless filenames? Claude Code’s Windows auto-detection prepends bash to any command with .sh, which would double-invoke bash.

Creating Custom Hooks

Step 1: Define Hook in hooks.json

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "'${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd' my-custom-hook",
            "async": false
          }
        ]
      }
    ]
  }
}

Step 2: Create Hook Script

Create hooks/my-custom-hook (no extension):
#!/usr/bin/env bash
set -euo pipefail

# Your custom logic here
echo "Running custom hook..."

# Output JSON for context injection
cat <<EOF
{
  "additional_context": "Custom context from my hook",
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Custom context"
  }
}
EOF

exit 0

Step 3: Make Executable

chmod +x hooks/my-custom-hook

Step 4: Test Locally

./hooks/run-hook.cmd my-custom-hook

Hook Output Format

Hooks must output valid JSON to stdout:
{
  "additional_context": "<string>",
  "hookSpecificOutput": {
    "hookEventName": "<event-name>",
    "additionalContext": "<string>",
    "customField": "<any-value>"
  }
}
  • additional_context: Text injected into agent context (Cursor format)
  • hookSpecificOutput.hookEventName: Name of triggering event
  • hookSpecificOutput.additionalContext: Text injected into agent context (Claude Code format)
  • Custom fields: Platform-specific extensions
Invalid JSON will cause hook failures. Use tools like jq to validate:
./hooks/run-hook.cmd session-start | jq .

Platform Support

PlatformHook SupportSessionStartCustom Hooks
Claude Code✅ Yes✅ Yes✅ Yes
Cursor✅ Yes✅ Yes✅ Yes
OpenCode❌ No✅ Via Plugin❌ No
Codex❌ No❌ No❌ No
OpenCode alternative: Uses plugin system with experimental.chat.system.transform hook to inject context at session start. See Plugin System.

Debugging Hooks

Enable Verbose Logging

Add debugging to your hook script:
#!/usr/bin/env bash
set -euo pipefail

# Log to file for debugging
exec 2>>/tmp/superpowers-hook-debug.log
echo "[$(date)] Hook started" >&2

# Your hook logic...

echo "[$(date)] Hook completed" >&2

Check Platform Logs

  • Claude Code: Check debug logs via /debug logs
  • Cursor: Check console in developer tools
  • Manual test: Run hook directly: ./hooks/run-hook.cmd session-start

Common Issues

  • Verify hooks.json syntax with jq
  • Check file permissions: chmod +x hooks/session-start
  • Ensure matcher pattern matches session state
  • Verify plugin is properly installed
  • Test JSON with: ./hooks/run-hook.cmd session-start | jq .
  • Check for unescaped special characters
  • Verify all quotes are properly escaped
  • Ensure heredoc EOF is on its own line
  • Verify both additional_context AND hookSpecificOutput.additionalContext are set
  • Check platform-specific field requirements
  • Test with minimal content first
  • Review platform logs for errors

Best Practices

1

Keep Hooks Fast

Hooks block session start - keep execution under 1 second:
# Add timeout for network operations
timeout 3s git fetch origin || true
2

Handle Errors Gracefully

Don’t fail the entire session if hook fails:
content=$(cat file.md 2>&1 || echo "Error: file not found")
3

Use Efficient String Escaping

Use bash parameter substitution instead of loops:
# Fast: single pass
s="${s//\\/\\\\}"

# Slow: character by character
for char in $s; do ...; done
4

Test Cross-Platform

Test on both Windows and Unix:
# Unix
bash hooks/run-hook.cmd session-start

# Windows
cmd /c hooks\run-hook.cmd session-start

Next Steps

Architecture

Understand overall system architecture

Plugin System

Learn how plugins integrate hooks

Creating Skills

Write custom skills that hooks can inject

Contributing

Contribute hook improvements

Build docs developers (and LLMs) love