7.7 KiB
Hook Mechanisms - Deep Dive
Technical deep dive into how the UserPromptSubmit and PreToolUse hooks work.
Table of Contents
- UserPromptSubmit Hook Flow
- PreToolUse Hook Flow
- Exit Code Behavior (CRITICAL)
- Session State Management
- Performance Considerations
UserPromptSubmit Hook Flow
Execution Sequence
User submits prompt
↓
.claude/settings.json registers hook
↓
skill-activation-prompt.sh executes
↓
npx tsx skill-activation-prompt.ts
↓
Hook reads stdin (JSON with prompt)
↓
Loads skill-rules.json
↓
Matches keywords + intent patterns
↓
Groups matches by priority (critical → high → medium → low)
↓
Outputs formatted message to stdout
↓
stdout becomes context for Claude (injected before prompt)
↓
Claude sees: [skill suggestion] + user's prompt
Key Points
- Exit code: Always 0 (allow)
- stdout: → Claude's context (injected as system message)
- Timing: Runs BEFORE Claude processes prompt
- Behavior: Non-blocking, advisory only
- Purpose: Make Claude aware of relevant skills
Input Format
{
"session_id": "abc-123",
"transcript_path": "/path/to/transcript.json",
"cwd": "/root/git/your-project",
"permission_mode": "normal",
"hook_event_name": "UserPromptSubmit",
"prompt": "how does the layout system work?"
}
Output Format (to stdout)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 SKILL ACTIVATION CHECK
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📚 RECOMMENDED SKILLS:
→ project-catalog-developer
ACTION: Use Skill tool BEFORE responding
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Claude sees this output as additional context before processing the user's prompt.
PreToolUse Hook Flow
Execution Sequence
Claude calls Edit/Write tool
↓
.claude/settings.json registers hook (matcher: Edit|Write)
↓
skill-verification-guard.sh executes
↓
npx tsx skill-verification-guard.ts
↓
Hook reads stdin (JSON with tool_name, tool_input)
↓
Loads skill-rules.json
↓
Checks file path patterns (glob matching)
↓
Reads file for content patterns (if file exists)
↓
Checks session state (was skill already used?)
↓
Checks skip conditions (file markers, env vars)
↓
IF MATCHED AND NOT SKIPPED:
Update session state (mark skill as enforced)
Output block message to stderr
Exit with code 2 (BLOCK)
ELSE:
Exit with code 0 (ALLOW)
↓
IF BLOCKED:
stderr → Claude sees message
Edit/Write tool does NOT execute
Claude must use skill and retry
IF ALLOWED:
Tool executes normally
Key Points
- Exit code 2: BLOCK (stderr → Claude)
- Exit code 0: ALLOW
- Timing: Runs BEFORE tool execution
- Session tracking: Prevents repeated blocks in same session
- Fail open: On errors, allows operation (don't break workflow)
- Purpose: Enforce critical guardrails
Input Format
{
"session_id": "abc-123",
"transcript_path": "/path/to/transcript.json",
"cwd": "/root/git/your-project",
"permission_mode": "normal",
"hook_event_name": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "/root/git/your-project/form/src/services/user.ts",
"old_string": "...",
"new_string": "..."
}
}
Output Format (to stderr when blocked)
⚠️ BLOCKED - Database Operation Detected
📋 REQUIRED ACTION:
1. Use Skill tool: 'database-verification'
2. Verify ALL table and column names against schema
3. Check database structure with DESCRIBE commands
4. Then retry this edit
Reason: Prevent column name errors in Prisma queries
File: form/src/services/user.ts
💡 TIP: Add '// @skip-validation' comment to skip future checks
Claude receives this message and understands it needs to use the skill before retrying the edit.
Exit Code Behavior (CRITICAL)
Exit Code Reference Table
| Exit Code | stdout | stderr | Tool Execution | Claude Sees |
|---|---|---|---|---|
| 0 (UserPromptSubmit) | → Context | → User only | N/A | stdout content |
| 0 (PreToolUse) | → User only | → User only | Proceeds | Nothing |
| 2 (PreToolUse) | → User only | → CLAUDE | BLOCKED | stderr content |
| Other | → User only | → User only | Blocked | Nothing |
Why Exit Code 2 Matters
This is THE critical mechanism for enforcement:
- Only way to send message to Claude from PreToolUse
- stderr content is "fed back to Claude automatically"
- Claude sees the block message and understands what to do
- Tool execution is prevented
- Critical for enforcement of guardrails
Example Conversation Flow
User: "Add a new user service with Prisma"
Claude: "I'll create the user service..."
[Attempts to Edit form/src/services/user.ts]
PreToolUse Hook: [Exit code 2]
stderr: "⚠️ BLOCKED - Use database-verification"
Claude sees error, responds:
"I need to verify the database schema first."
[Uses Skill tool: database-verification]
[Verifies column names]
[Retries Edit - now allowed (session tracking)]
Session State Management
Purpose
Prevent repeated nagging in the same session - once Claude uses a skill, don't block again.
State File Location
.claude/hooks/state/skills-used-{session_id}.json
State File Structure
{
"skills_used": [
"database-verification",
"error-tracking"
],
"files_verified": []
}
How It Works
-
First edit of file with Prisma:
- Hook blocks with exit code 2
- Updates session state: adds "database-verification" to skills_used
- Claude sees message, uses skill
-
Second edit (same session):
- Hook checks session state
- Finds "database-verification" in skills_used
- Exits with code 0 (allow)
- No message to Claude
-
Different session:
- New session ID = new state file
- Hook blocks again
Limitation
The hook cannot detect when the skill is actually invoked - it just blocks once per session per skill. This means:
- If Claude doesn't use the skill but makes a different edit, it won't block again
- Trust that Claude follows the instruction
- Future enhancement: detect actual Skill tool usage
Performance Considerations
Target Metrics
- UserPromptSubmit: < 100ms
- PreToolUse: < 200ms
Performance Bottlenecks
-
Loading skill-rules.json (every execution)
- Future: Cache in memory
- Future: Watch for changes, reload only when needed
-
Reading file content (PreToolUse)
- Only when contentPatterns configured
- Only if file exists
- Can be slow for large files
-
Glob matching (PreToolUse)
- Regex compilation for each pattern
- Future: Compile once, cache
-
Regex matching (Both hooks)
- Intent patterns (UserPromptSubmit)
- Content patterns (PreToolUse)
- Future: Lazy compile, cache compiled regexes
Optimization Strategies
Reduce patterns:
- Use more specific patterns (fewer to check)
- Combine similar patterns where possible
File path patterns:
- More specific = fewer files to check
- Example:
form/src/services/**better thanform/**
Content patterns:
- Only add when truly necessary
- Simpler regex = faster matching
Related Files:
- SKILL.md - Main skill guide
- TROUBLESHOOTING.md - Debug hook issues
- SKILL_RULES_REFERENCE.md - Configuration reference