| name | hook-authoring |
| description | | Use when this capability is needed. |
Hook Authoring Patterns
This skill auto-applies when you're working with Claude Code hooks. Follow these patterns for consistent, reliable hooks.
Hook Lifecycle
Session Start
│
├── SessionStart hook
│
▼
┌─────────────────────────────────┐
│ User sends prompt │
│ │ │
│ ├── UserPromptSubmit hooks │
│ ▼ │
│ Claude processes... │
│ │ │
│ ├── Stop hook │
│ ▼ │
│ (repeat) │
└─────────────────────────────────┘
│
├── PreCompact hook
│
▼
Session End
│
└── SessionEnd hook
Required Patterns
1. Script Header
Always start with:
#!/bin/bash
set -euo pipefail
2. Consume stdin
Critical: All hooks MUST consume stdin to avoid broken pipe errors:
input=$(cat)
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id // empty')
3. Graceful Degradation
Hooks should work even when dependencies are missing:
if ! command -v jq &>/dev/null; then
exit 0
fi
if ! command -v agent-event-bus-cli &>/dev/null; then
cli_path="$HOME/.local/bin/agent-event-bus-cli"
[[ -x "$cli_path" ]] || exit 0
fi
if [[ -z "${ZELLIJ:-}" ]]; then
exit 0
fi
4. Exit Codes
exit 0 - Success (normal completion)
- Non-zero exits don't block Claude but may show error messages
- Prefer silent
exit 0 for graceful degradation
Input JSON Format
All hooks receive JSON on stdin:
{
"session_id": "uuid",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory"
}
Additional fields by trigger:
- SessionStart:
permission_mode, source ("user" or "resume")
- SessionEnd:
permission_mode, reason
- PreCompact:
trigger
Output Patterns
Structured Data
Use XML tags for data Claude should parse:
echo "<recent-events>"
echo "$events"
echo "</recent-events>"
Status Messages
Simple text output works:
echo "Hook completed successfully"
Silent Hooks
For hooks that only have side effects (like zjstatus notifications):
zellij pipe "zjstatus::notify::message" 2>/dev/null || true
Zellij Integration
When interacting with zellij:
[[ -z "${ZELLIJ:-}" ]] && exit 0
zellij action rename-tab "$tab_name" 2>/dev/null || true
zellij pipe "zjstatus::notify::message" 2>/dev/null || true
zellij pipe "zjstatus::notify::" 2>/dev/null || true
For worktree paths, show repo (branch) format:
if [[ "$cwd" == */.worktrees/* ]]; then
repo=$(basename "$(dirname "$(dirname "$cwd")")")
branch=$(basename "$cwd")
tab_name="$repo ($branch)"
fi
Event Bus Integration
For hooks that interact with the event bus:
if command -v agent-event-bus-cli &>/dev/null; then
cli="agent-event-bus-cli"
elif [[ -x "$HOME/.local/bin/agent-event-bus-cli" ]]; then
cli="$HOME/.local/bin/agent-event-bus-cli"
else
exit 0
fi
"$cli" register --name "$session_name" --client-id "$session_id"
"$cli" publish --type "event_type" --payload "message" --session-id "$session_id"
"$cli" events --resume --session-id "$session_id"
Testing
Run make test-hooks to test all hooks. Tests verify:
- Script syntax is valid
- Scripts are executable
- Graceful degradation works (missing deps don't crash)
Add new test cases in tests/test-hooks.sh.
Configuration
Register hooks in settings.json:
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/your-hook.sh" }] }]
}
}
Available triggers:
SessionStart - Session begins
SessionEnd - Session ends
UserPromptSubmit - User sends message
Stop - Claude finishes response
PreCompact - Before context summarization
Reference
See existing hooks in home/.claude/hooks/ for examples:
session-start.sh - Event bus registration, zellij tab rename
session-end.sh - Cleanup
prompt-events.sh - Incremental event polling
zj-status.sh - Visual state indicator (zjstatus notification)
pre-compact.sh - WIP checkpointing
Converted and distributed by TomeVault — claim your Tome and manage your conversions.