hook-authoring
Workflow Routing (SYSTEM PROMPT)
When user requests creating a new hook: Examples: "create a hook for X", "add a hook", "make a SessionStart hook" -> READ: ${PAI_DIR}/skills/hook-authoring/workflows/create-hook.md -> EXECUTE: Follow hook creation workflow
When user needs to debug hooks: Examples: "hook not working", "debug hooks", "hook troubleshooting" -> READ: ${PAI_DIR}/skills/hook-authoring/workflows/debug-hooks.md -> EXECUTE: Run debugging checklist
Claude Code Hook Events
SessionStart
When: New Claude Code session begins Use Cases: Load context, initialize state, capture metadata
PreToolUse
When: Before a tool executes Use Cases: Security checks, permission enforcement, input validation
PostToolUse
When: After a tool executes Use Cases: Logging, metrics, audit trail
UserPromptSubmit
When: User submits a prompt Use Cases: Pre-processing, context injection, tab updates
Stop
When: Claude completes a response (not user) Use Cases: Extract completion info, update tab titles, capture work
SubagentStop
When: A subagent (Task) completes Use Cases: Agent tracking, result capture, coordination
ConfigChange
When: Settings or config files change Use Cases: Sync validation, drift detection
Hook Configuration
Location: ${PAI_DIR}/.claude/settings.json or ~/.claude/settings.json
{
"hooks": {
"SessionStart": [
{
"matcher": {},
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/hooks/my-hook.ts"
}
]
}
]
}
}
Hook Input/Output
Input (stdin JSON)
interface HookInput {
session_id: string;
transcript_path: string;
// Event-specific fields...
}
Output (stdout)
- Continue:
{ "continue": true } - Block:
{ "continue": false, "reason": "..." } - Inject content:
{ "result": "<system-reminder>...</system-reminder>" } - Add context (CC 2.1.9+):
{ "decision": "continue", "additionalContext": "..." }
Session ID Tracking (CC 2.1.9+)
Hooks receive session_id in input JSON. For persistent storage:
// Use session_id from hook input
const sessionFile = `${PAI_DIR}/state/sessions/${input.session_id}.json`;
// Or use ${CLAUDE_SESSION_ID} substitution in settings.json:
{
"hooks": {
"SessionStart": [{
"hooks": [{
"type": "command",
"command": "${PAI_DIR}/hooks/init-session.ts --session ${CLAUDE_SESSION_ID}"
}]
}]
}
}
PAI Active Hooks
| Hook | Event | Purpose |
|---|---|---|
session-start.ts |
SessionStart | Load CORE skill, initialize session |
pre-tool-use-security.ts |
PreToolUse | Block dangerous Bash commands |
post-tool-use.ts |
PostToolUse | JSONL tool usage logging |
update-tab-titles.ts |
UserPromptSubmit | Set terminal tab titles |
stop-hook.ts |
Stop | Checkpoint logging, tab update |
config-change.ts |
ConfigChange | Settings sync validation |
Hook Best Practices
- Fail gracefully - Hooks should never block Claude
- Timeout protection - Use timeouts for external calls
- Async by default - Don't block the main process
- TypeScript preferred - Use Bun for execution
- Log errors - Don't fail silently
Quick Hook Template
#!/usr/bin/env bun
// ${PAI_DIR}/hooks/my-hook.ts
import { readFileSync } from 'fs';
const input = JSON.parse(readFileSync(0, 'utf-8'));
// Your logic here
// Output (choose one):
console.log(JSON.stringify({ continue: true }));
// console.log(JSON.stringify({ result: "<system-reminder>...</system-reminder>" }));
Make executable: chmod +x my-hook.ts
Related
- See
post-tool-use.tsfor JSONL logging pattern - See
hook-testskill for hook health checking