Branch skill for building and improving hooks. Use when creating new hooks, adapting marketplace hooks, validating hook structure, writing hook scripts, or improving existing hooks. Triggers: 'create hook', 'improve hook', 'validate hook', 'fix hook', 'PreToolUse', 'PostToolUse', 'Stop hook', 'hook script', 'adapt hook', 'prompt hook', 'command hook'.
Builds and improves Claude Code hooks using prompt-based or command-based implementations. Triggers on requests to create, adapt, validate, or fix hooks for PreToolUse, Stop, SessionStart, and other events.
/plugin marketplace add henmessi/plugin-dev/plugin install henmessi-plugin-dev@henmessi/plugin-devThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Build and improve hooks following the hooks-management policy.
Primary policy: JARVIS-05 → .claude/skills/hooks-management/SKILL.md
This branch executes the policy defined by JARVIS-05. Always sync with Primary before major operations.
Task Received
│
├── Create new hook? ────────────────> Workflow 1: Build
│ └── What type?
│ ├── Context-aware logic ───────> Prompt hook (Recommended)
│ ├── Validate/block ────────────> Command hook with exit 2
│ ├── Log/audit ─────────────────> Command hook with exit 0
│ └── File/external access ──────> Command hook
│
├── Adapt marketplace hook? ─────────> Workflow 3: Adapt
│
├── Fix existing hook? ──────────────> Workflow 2: Improve
│
└── Validate hook? ──────────────────> Validation Checklist
Use LLM-driven decision making for context-aware validation:
{
"type": "prompt",
"prompt": "Evaluate if this tool use is appropriate: $TOOL_INPUT",
"timeout": 30
}
Supported events: Stop, SubagentStop, UserPromptSubmit, PreToolUse
Benefits:
Access input variables:
$TOOL_INPUT - Full tool input JSON$TOOL_RESULT - Tool result (PostToolUse)$USER_PROMPT - User's prompt text$TRANSCRIPT_PATH - Path to session transcriptExecute bash commands for deterministic checks:
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 60
}
Use for:
| Type | Implementation | Use When | Output |
|---|---|---|---|
| prompt | Inline text | Complex logic, context-aware | LLM decision |
| command | Bash script | File access, validation, external tools | stdout/stderr + exit code |
For plugin hooks in hooks/hooks.json, use wrapper format:
{
"description": "Brief explanation of hooks (optional)",
"hooks": {
"PreToolUse": [...],
"Stop": [...],
"SessionStart": [...]
}
}
Key points:
description field is optionalhooks field is required wrapper containing actual hook eventsFor user settings in .claude/settings.json, use direct format:
{
"PreToolUse": [...],
"Stop": [...],
"SessionStart": [...]
}
Key points:
Important: Plugin hooks.json requires the {"hooks": {...}} wrapper. Settings.json does not.
| Event | When | Use For | Supports Prompt? |
|---|---|---|---|
| PreToolUse | Before tool executes | Validate, block, modify | ✓ |
| PostToolUse | After tool completes | Log, react, chain actions | ✗ |
| Stop | Before session ends | Completeness check | ✓ |
| SubagentStop | Before subagent stops | Task validation | ✓ |
| UserPromptSubmit | Before prompt sent | Add context, validate | ✓ |
| SessionStart | Session begins | Initialize environment | ✗ |
| SessionEnd | Session ends | Cleanup, logging | ✗ |
| PreCompact | Before context compaction | Preserve critical info | ✗ |
| Notification | On notifications | Alert routing | ✗ |
Execute before any tool runs. Use to approve, deny, or modify tool calls.
Prompt-based example:
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "Validate file write safety. Check: system paths, credentials, path traversal, sensitive content. Return 'approve' or 'deny'."
}
]
}
]
}
Output format for PreToolUse:
{
"hookSpecificOutput": {
"permissionDecision": "allow|deny|ask",
"updatedInput": {"field": "modified_value"}
},
"systemMessage": "Explanation for Claude"
}
Execute when main agent considers stopping. Use to validate completeness.
Example:
{
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "prompt",
"prompt": "Verify task completion: tests run, build succeeded, questions answered. Return 'approve' to stop or 'block' with reason to continue."
}
]
}
]
}
Decision output:
{
"decision": "approve|block",
"reason": "Explanation",
"systemMessage": "Additional context"
}
Execute when Claude Code session begins. Use to load context and set environment.
Example:
{
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh"
}
]
}
]
}
Special capability: Persist environment variables using $CLAUDE_ENV_FILE:
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 1
# Detect project type and persist
if [ -f "package.json" ]; then
echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
elif [ -f "Cargo.toml" ]; then
echo "export PROJECT_TYPE=rust" >> "$CLAUDE_ENV_FILE"
fi
{
"continue": true,
"suppressOutput": false,
"systemMessage": "Message for Claude",
"hookSpecificOutput": {
"permissionDecision": "allow|deny|ask",
"updatedInput": {"field": "modified_value"}
}
}
continue: If false, halt processing (default true)suppressOutput: Hide output from transcript (default false)systemMessage: Message shown to ClaudehookSpecificOutput: Event-specific data (PreToolUse only)| Exit Code | Meaning | Effect |
|---|---|---|
| 0 | Success | stdout shown in transcript |
| 2 | Block | stderr fed to Claude, action blocked |
| Other | Error | Logged, continues execution |
All hooks receive JSON via stdin with common fields:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.txt",
"cwd": "/current/working/dir",
"permission_mode": "ask|allow",
"hook_event_name": "PreToolUse"
}
Event-specific fields:
tool_name, tool_input, tool_resultuser_promptreasonAvailable in all command hooks:
| Variable | Description | Available |
|---|---|---|
$CLAUDE_PROJECT_DIR | Project root path | Always |
$CLAUDE_PLUGIN_ROOT | Plugin directory (use for portable paths) | Always |
$CLAUDE_ENV_FILE | File to persist env vars | SessionStart only |
$CLAUDE_CODE_REMOTE | Set if running in remote context | When remote |
Always use ${CLAUDE_PLUGIN_ROOT} in hook commands for portability:
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
}
Exact match:
"matcher": "Write"
Multiple tools:
"matcher": "Read|Write|Edit"
Wildcard (all tools):
"matcher": "*"
Regex patterns:
"matcher": "mcp__.*__delete.*"
Common patterns:
// All MCP tools
"matcher": "mcp__.*"
// Specific plugin's MCP tools
"matcher": "mcp__plugin_asana_.*"
// All file operations
"matcher": "Read|Write|Edit"
// Bash commands only
"matcher": "Bash"
Note: Matchers are case-sensitive.
Answer these questions:
Need context-aware logic? ───Yes──> prompt hook (Recommended)
Need file access? ───Yes──> command hook
Need external tools? ───Yes──> command hook
Need fast deterministic? ───Yes──> command hook
Just add text? ───Yes──> prompt hook
Prompt Hook (Recommended for most cases):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "File path: $TOOL_INPUT.file_path. Verify: 1) Not in /etc or system directories 2) Not .env or credentials 3) Path doesn't contain '..' traversal. Return 'approve' or 'deny'.",
"timeout": 30
}
]
}
]
}
}
Command Hook:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/validate-write.sh",
"timeout": 10
}
]
}
]
}
}
Script Template:
#!/bin/bash
# Hook: [Name]
# Event: [PreToolUse/PostToolUse/etc]
# Purpose: [What this hook does]
set -euo pipefail
# Read JSON input from stdin
input=$(cat)
# Parse input using jq with null safety
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
session_id=$(echo "$input" | jq -r '.session_id // empty')
# Validate input exists
if [[ -z "$file_path" ]]; then
echo "Warning: No file path provided" >&2
exit 0 # Don't block on missing input
fi
# Validation logic
if [[ "$file_path" == *.env* ]]; then
# Block: output to stderr, exit 2
echo "BLOCKED: Cannot write to .env files" >&2
exit 2
fi
# Success: output to stdout, exit 0
echo "Validated: $file_path"
exit 0
Cross-Platform Wrapper (for Windows compatibility):
#!/bin/bash
# Polyglot wrapper - works on Windows (Git Bash) and Unix
# Detect platform
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
# Windows path handling
file_path=$(echo "$file_path" | sed 's|\\|/|g')
fi
# Rest of script...
{
"timeout": 10
}
Recommended timeouts:
| Hook Type | Use Case | Timeout |
|---|---|---|
| Prompt | Default | 30s |
| Command | Simple validation | 5-10s |
| Command | File operations | 10-30s |
| Command | External API calls | 30-60s |
Run full validation checklist.
# Check hook configuration
cat .claude/settings.json | jq '.hooks'
# Or for plugin hooks
cat hooks/hooks.json
# Check script exists and is executable
ls -la hooks/scripts/
file hooks/scripts/*.sh
| Component | Check | Common Issues |
|---|---|---|
| Configuration | Valid JSON? | Missing timeout, wrong event name |
| Format | Plugin vs Settings? | Using wrong wrapper |
| Script path | Exists? Executable? | Wrong path, not chmod +x |
| Exit codes | Correct meaning? | Using exit 1 instead of exit 2 |
| Input parsing | jq correct? | Missing // empty for null safety |
| Error handling | Edge cases? | Script fails on unexpected input |
| Timeout | Appropriate? | Too short or missing |
Adding timeout:
{
"type": "command",
"command": "script.sh",
"timeout": 30
}
Adding error handling to script:
# Safe jq parsing with defaults
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
if [[ -z "$file_path" ]]; then
echo "Warning: No file path provided" >&2
exit 0 # Don't block on missing input
fi
Fixing exit codes:
# Wrong: exit 1 (error, continues)
# Right: exit 2 (block action)
exit 2
Switching to prompt-based (recommended):
{
"type": "prompt",
"prompt": "Validate this file write operation. File: $TOOL_INPUT.file_path. Check for system paths, credentials, and path traversal. Return 'approve' or 'deny'."
}
--debug flag/hooks command to review loaded hooksUpdate comments in script with date and changes.
When taking a hook from wshobson-agents, obra-superpowers, or similar:
# Check configuration
cat marketplace-plugin/hooks/hooks.json
# Check scripts
ls marketplace-plugin/hooks/scripts/
cat marketplace-plugin/hooks/scripts/*.sh
| Original Purpose | JARVIS Application |
|---|---|
| Code validation | Adapt for JARVIS coding standards |
| File protection | Adapt for category-specific paths |
| Logging | Add BigQuery logging integration |
| Context injection | Add JARVIS-specific context |
| Stop validation | Add JARVIS completion criteria |
Original (may be command-based):
{
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "validate.sh"
}
]
}
]
}
Adapted for JARVIS (prefer prompt-based):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "prompt",
"prompt": "JARVIS file write validation. Path: $TOOL_INPUT.file_path. Check: 1) Not modifying Primary Skills without policy sync 2) Not overwriting CLAUDE.md identity 3) Path is within category bounds. Return 'approve' or 'deny'.",
"timeout": 30
}
]
}
]
}
}
Add JARVIS-specific validation:
#!/bin/bash
# Adapted from [source] for JARVIS ecosystem
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# Original validation
if [[ "$file_path" == *.env* ]]; then
echo "BLOCKED: Cannot write to .env files" >&2
exit 2
fi
# JARVIS-specific: Protect Primary Skills
if [[ "$file_path" == *"/.claude/skills/"*"/SKILL.md" ]]; then
echo "WARNING: Modifying Primary Skill - ensure policy compliance" >&2
# Don't block, just warn
fi
# JARVIS-specific: Protect CLAUDE.md
if [[ "$file_path" == *"/CLAUDE.md" ]]; then
echo "INFO: Updating category identity file"
fi
exit 0
Run full validation checklist.
Combine command and prompt hooks for layered validation:
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/quick-check.sh",
"timeout": 5
},
{
"type": "prompt",
"prompt": "Deep analysis of bash command: $TOOL_INPUT",
"timeout": 15
}
]
}
]
}
All hooks run in parallel - design for independence.
Create hooks that activate conditionally:
#!/bin/bash
FLAG_FILE="$CLAUDE_PROJECT_DIR/.enable-strict-validation"
if [ ! -f "$FLAG_FILE" ]; then
exit 0 # Skip when disabled
fi
# Flag present, run validation
input=$(cat)
# ... validation logic ...
#!/bin/bash
CONFIG_FILE="$CLAUDE_PROJECT_DIR/.claude/plugin-config.json"
if [ -f "$CONFIG_FILE" ]; then
strict_mode=$(jq -r '.strictMode // false' "$CONFIG_FILE")
if [ "$strict_mode" != "true" ]; then
exit 0
fi
fi
# Strict mode enabled, run validation
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
cache_key=$(echo -n "$file_path" | md5sum | cut -d' ' -f1)
cache_file="/tmp/hook-cache-$cache_key"
# Check cache (5 minute TTL)
if [ -f "$cache_file" ]; then
cache_age=$(($(date +%s) - $(stat -c%Y "$cache_file" 2>/dev/null || stat -f%m "$cache_file")))
if [ "$cache_age" -lt 300 ]; then
cat "$cache_file"
exit 0
fi
fi
# Perform validation
result='{"decision": "approve"}'
echo "$result" > "$cache_file"
echo "$result"
SessionStart - Initialize tracking:
#!/bin/bash
echo "0" > /tmp/test-count-$$
PostToolUse - Track events:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
if [[ "$tool_name" == "Bash" ]]; then
command=$(echo "$input" | jq -r '.tool_result')
if [[ "$command" == *"test"* ]]; then
count=$(cat /tmp/test-count-$$ 2>/dev/null || echo "0")
echo $((count + 1)) > /tmp/test-count-$$
fi
fi
Stop - Verify based on tracking:
#!/bin/bash
test_count=$(cat /tmp/test-count-$$ 2>/dev/null || echo "0")
if [ "$test_count" -eq 0 ]; then
echo '{"decision": "block", "reason": "No tests were run"}' >&2
exit 2
fi
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# Block system directories
if [[ "$file_path" == /etc/* ]] || [[ "$file_path" == /sys/* ]]; then
echo "BLOCKED: Cannot write to system directories" >&2
exit 2
fi
# Block secrets files
if [[ "$file_path" == *.env* ]] || [[ "$file_path" == *credentials* ]]; then
echo "BLOCKED: Cannot write to secrets files" >&2
exit 2
fi
exit 0
#!/bin/bash
input=$(cat)
timestamp=$(date -Iseconds)
tool_name=$(echo "$input" | jq -r '.tool_name // "unknown"')
session_id=$(echo "$input" | jq -r '.session_id // "unknown"')
echo "$timestamp | $session_id | $tool_name" >> /var/log/claude-ops.log
exit 0
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
# Only validate MCP tools
if [[ "$tool_name" != mcp__* ]]; then
exit 0
fi
# Check if MCP server is in allowed list
mcp_server=$(echo "$tool_name" | cut -d'_' -f3)
allowed_servers="vault supabase-common bigquery"
if [[ ! " $allowed_servers " =~ " $mcp_server " ]]; then
echo "BLOCKED: MCP server '$mcp_server' not in allowed list" >&2
exit 2
fi
exit 0
{
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "prompt",
"prompt": "Session started. Remember:\n- Read CLAUDE.md first\n- Follow Primary Skill policies\n- Use improvement cycle every ~6 sessions"
}
]
}
]
}
{
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "prompt",
"prompt": "Review transcript. Verify: 1) Tests run after code changes 2) Build succeeded 3) All questions answered 4) No unfinished work. Return 'approve' only if complete."
}
]
}
]
}
Important: Hooks are loaded when Claude Code session starts. Changes to hook configuration require restarting Claude Code.
Cannot hot-swap hooks:
hooks/hooks.json won't affect current sessionclaude againTo test hook changes:
claude or ccclaude --debugHooks are validated when Claude Code starts:
Use /hooks command to review loaded hooks in current session.
claude --debug
Look for hook registration, execution logs, input/output JSON, and timing information.
echo '{"tool_name": "Write", "tool_input": {"file_path": "/test"}}' | \
bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh
echo "Exit code: $?"
output=$(./your-hook.sh < test-input.json)
echo "$output" | jq .
Use /hooks command in Claude Code to see which hooks are active.
command or promptchmod +x)#!/bin/bash)${CLAUDE_PLUGIN_ROOT} for portability// empty for null safety"$var" not $var)claude --debug| Issue | Diagnosis | Fix |
|---|---|---|
| Hook not triggering | Event name wrong | Check spelling: PreToolUse not preToolUse |
| Script not found | Path wrong | Use ${CLAUDE_PLUGIN_ROOT} for portability |
| No blocking effect | Wrong exit code | Use exit 2, not exit 1 |
| JSON parse error | jq syntax wrong | Add // empty for null safety |
| Timeout errors | Script too slow | Increase timeout or optimize script |
| Windows fails | Path separators | Use polyglot wrapper, convert \ to / |
| Hook not loading | Wrong format | Plugin needs {"hooks": {...}} wrapper |
| Changes not applied | Session not restarted | Exit and restart Claude Code |
DO:
"$var")set -euo pipefail in scriptsDON'T:
Before executing any workflow:
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.