Hooks
Hooks let capabilities register automated scripts that run at specific points in the Claude Code agent lifecycle. When you run omnidev sync, hooks from all enabled capabilities are merged and written to .claude/settings.json.
This enables powerful automation: validating commands before execution, running linters after file edits, injecting context at session start, and more.
Quick Start
Section titled “Quick Start”Create a hooks/hooks.toml file in your capability:
# Validate bash commands before execution[[PreToolUse]]matcher = "Bash"[[PreToolUse.hooks]]type = "command"command = "${OMNIDEV_CAPABILITY_ROOT}/hooks/validate-bash.sh"timeout = 30Then add your validation script at hooks/validate-bash.sh:
#!/bin/bash# Read JSON input from stdinINPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# Block dangerous patternsif echo "$COMMAND" | grep -qE 'rm -rf /|dd if='; then echo "Dangerous command blocked" >&2 exit 2 # Exit code 2 blocks the toolfi
exit 0 # Allow the commandMake it executable:
chmod +x hooks/validate-bash.shCapability Structure
Section titled “Capability Structure”Place hooks configuration and scripts in the hooks/ directory:
my-capability/├── omni.toml├── hooks/│ ├── hooks.toml # Hook configuration│ ├── validate-bash.sh # PreToolUse script│ └── run-linter.sh # PostToolUse script└── ...Configuration Format
Section titled “Configuration Format”Hooks are defined in TOML with a simple structure:
# Each event is a TOML array of tables[[PreToolUse]]matcher = "Bash" # Regex pattern for tool names[[PreToolUse.hooks]] # Array of hooks to runtype = "command"command = "${OMNIDEV_CAPABILITY_ROOT}/hooks/validate-bash.sh"timeout = 30 # Optional, in seconds
# Multiple matchers per event[[PostToolUse]]matcher = "Write|Edit" # Match Write OR Edit tools[[PostToolUse.hooks]]type = "command"command = "${OMNIDEV_PROJECT_DIR}/.omni/hooks/lint.sh"Hook Events
Section titled “Hook Events”OmniDev supports all 10 Claude Code hook events:
Tool Execution Events
Section titled “Tool Execution Events”| Event | When it runs | Supports Matcher | Supports Prompt |
|---|---|---|---|
PreToolUse | Before a tool executes | Yes | Yes |
PostToolUse | After a tool completes | Yes | No |
PermissionRequest | Before permission prompt shown | Yes | Yes |
Workflow Events
Section titled “Workflow Events”| Event | When it runs | Supports Matcher | Supports Prompt |
|---|---|---|---|
UserPromptSubmit | Before user prompt processed | No | Yes |
Stop | When main agent finishes | No | Yes |
SubagentStop | When subagent finishes | No | Yes |
Notification | When notifications sent | Yes | No |
Session Events
Section titled “Session Events”| Event | When it runs | Supports Matcher | Supports Prompt |
|---|---|---|---|
SessionStart | When session starts/resumes | Yes | No |
SessionEnd | When session ends | No | No |
PreCompact | Before context compaction | Yes | No |
Matchers
Section titled “Matchers”Matchers filter which tools or events trigger a hook. They use regex patterns.
Tool Matchers
Section titled “Tool Matchers”[[PreToolUse]]matcher = "Bash" # Exact match
[[PreToolUse]]matcher = "Edit|Write" # Match Edit OR Write
[[PreToolUse]]matcher = "mcp__.*" # All MCP tools
[[PreToolUse]]matcher = ".*" # All tools (or omit matcher)Common tool names: Bash, Read, Write, Edit, Glob, Grep, Task, WebFetch, WebSearch, NotebookEdit, LSP, TodoWrite, AskUserQuestion
Event-Specific Matchers
Section titled “Event-Specific Matchers”Notification event:
permission_prompt- Permission promptsidle_prompt- Idle promptsauth_success- Auth success notificationselicitation_dialog- Elicitation dialogs
[[Notification]]matcher = "permission_prompt"[[Notification.hooks]]type = "command"command = "./notify.sh"SessionStart event:
startup- Initial session startresume- Session resumedclear- Session clearedcompact- After compaction
[[SessionStart]]matcher = "startup|resume"[[SessionStart.hooks]]type = "command"command = "./init-session.sh"PreCompact event:
manual- Manual compactionauto- Auto compaction
Events Without Matchers
Section titled “Events Without Matchers”For UserPromptSubmit, Stop, SubagentStop, and SessionEnd, the matcher field is ignored:
[[Stop]][[Stop.hooks]]type = "command"command = "./cleanup.sh"Hook Types
Section titled “Hook Types”Command Hooks
Section titled “Command Hooks”Execute a shell command. Available for all events.
[[PreToolUse.hooks]]type = "command"command = "${OMNIDEV_CAPABILITY_ROOT}/hooks/validate.sh"timeout = 60 # Default: 60 secondsPrompt Hooks
Section titled “Prompt Hooks”Use LLM evaluation. Only available for: PreToolUse, PermissionRequest, UserPromptSubmit, Stop, SubagentStop.
[[PermissionRequest.hooks]]type = "prompt"prompt = "Review this permission request. Is it safe? Respond with JSON: {\"ok\": true} or {\"ok\": false, \"reason\": \"explanation\"}"timeout = 30 # Default: 30 secondsEnvironment Variables
Section titled “Environment Variables”OmniDev uses its own variable naming that gets transformed during sync:
| In hooks.toml | In settings.json | Description |
|---|---|---|
${OMNIDEV_CAPABILITY_ROOT} | ${CLAUDE_PLUGIN_ROOT} | Capability’s root directory |
${OMNIDEV_PROJECT_DIR} | ${CLAUDE_PROJECT_DIR} | Project root |
Always use OMNIDEV_ prefixed variables in your hooks.toml. They’re automatically transformed when written to .claude/settings.json.
Additional variables available at runtime:
CLAUDE_ENV_FILE- (SessionStart only) File path for persisting environment variables
Writing Hook Scripts
Section titled “Writing Hook Scripts”Input Format
Section titled “Input Format”All hooks receive JSON via stdin:
{ "session_id": "abc123", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/project/path", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "npm test", "description": "Run tests" }}Exit Codes
Section titled “Exit Codes”- Exit 0: Success, tool continues
- Exit 2: Blocking error, tool is prevented
- Other codes: Non-blocking error, logged but tool continues
Simple Example
Section titled “Simple Example”#!/bin/bash# Read JSON inputINPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# Block dangerous commandsif echo "$COMMAND" | grep -qE 'rm -rf /'; then echo "Dangerous command blocked" >&2 exit 2fi
exit 0Advanced JSON Output
Section titled “Advanced JSON Output”For fine-grained control, output JSON to stdout with exit code 0:
{ "continue": true, "systemMessage": "Warning: this command modifies production data", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Auto-approved safe operation" }}Permission decisions (PreToolUse only):
"allow"- Auto-approve without prompting"deny"- Block the operation"ask"- Show normal permission prompt
Python Example
Section titled “Python Example”#!/usr/bin/env python3import jsonimport sys
# Read inputinput_data = json.load(sys.stdin)tool_name = input_data.get("tool_name", "")tool_input = input_data.get("tool_input", {})
# Auto-approve reading documentation filesif tool_name == "Read": file_path = tool_input.get("file_path", "") if file_path.endswith((".md", ".txt", ".json")): output = { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Documentation file" } } print(json.dumps(output)) sys.exit(0)
sys.exit(0) # Default: continue normallyHow Sync Works
Section titled “How Sync Works”When you run omnidev sync:
- OmniDev loads
hooks/hooks.tomlfrom each enabled capability - Validates TOML structure, types, and regex patterns
- Transforms
OMNIDEV_variables toCLAUDE_variables - Merges hooks from all capabilities
- Writes to
.claude/settings.json
Merged Output
Section titled “Merged Output”If two capabilities define hooks, they’re combined:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/validate-bash.sh" }] }, { "matcher": "Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/lint-on-write.sh" }] } ] }}Common Patterns
Section titled “Common Patterns”Pre-commit Style Validation
Section titled “Pre-commit Style Validation”Block dangerous bash commands:
[[PreToolUse]]matcher = "Bash"[[PreToolUse.hooks]]type = "command"command = "${OMNIDEV_CAPABILITY_ROOT}/hooks/validate-bash.sh"timeout = 30Auto-format on Save
Section titled “Auto-format on Save”Run formatter after file edits:
[[PostToolUse]]matcher = "Write|Edit"[[PostToolUse.hooks]]type = "command"command = "${OMNIDEV_PROJECT_DIR}/node_modules/.bin/prettier --write"Context Loading at Session Start
Section titled “Context Loading at Session Start”Load project context when session begins:
[[SessionStart]]matcher = "startup|resume"[[SessionStart.hooks]]type = "command"command = "${OMNIDEV_CAPABILITY_ROOT}/hooks/load-context.sh"Context script example:
#!/bin/bash# Output is added as context to Claudeecho "Project: $(basename $PWD)"echo "Branch: $(git branch --show-current 2>/dev/null || echo 'not a git repo')"echo "Node: $(node --version 2>/dev/null || echo 'not installed')"exit 0Environment Variable Persistence
Section titled “Environment Variable Persistence”Set environment variables for the session:
#!/bin/bash# SessionStart hookif [ -n "$CLAUDE_ENV_FILE" ]; then echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE" echo 'export DEBUG=true' >> "$CLAUDE_ENV_FILE"fiexit 0User Prompt Validation
Section titled “User Prompt Validation”Block prompts containing secrets:
[[UserPromptSubmit]][[UserPromptSubmit.hooks]]type = "command"command = "${OMNIDEV_CAPABILITY_ROOT}/hooks/check-secrets.sh"#!/bin/bashINPUT=$(cat)PROMPT=$(echo "$INPUT" | jq -r '.prompt')
if echo "$PROMPT" | grep -qiE 'password=|api_key=|secret='; then echo '{"decision": "block", "reason": "Prompt may contain secrets"}' exit 0fi
exit 0Validation
Section titled “Validation”OmniDev validates hooks during capability loading:
- Unknown events are rejected
- Invalid hook types are rejected
- Prompt hooks on unsupported events are rejected
- Invalid regex patterns are rejected
CLAUDE_variables are transformed toOMNIDEV_(with warning)- Missing command/prompt fields are flagged
Run omnidev doctor to check for hook validation issues.
Security Considerations
Section titled “Security Considerations”Best practices:
- Validate and sanitize inputs - Never trust input blindly
- Quote shell variables - Use
"$VAR"not$VAR - Check for path traversal - Block
..in file paths - Use absolute paths - Specify full paths for scripts
- Skip sensitive files - Avoid processing
.env,.git/, keys - Test scripts independently - Ensure they work before integrating
Debugging
Section titled “Debugging”Enable Claude Code debug mode to see hook execution:
claude --debugDebug output shows:
[DEBUG] Executing hooks for PreToolUse:Bash[DEBUG] Found 1 hook matchers[DEBUG] Matched 1 hooks[DEBUG] Hook command completed with status 0Related
Section titled “Related”- Core Commands -
syncanddoctorcommands - Capability Structure - How to organize capabilities
- Claude Code Hooks Documentation - Full hooks specification