Hooks
Hooks let capabilities register automated scripts that run at specific points in the agent lifecycle. When you run omnidev sync, OmniDev reads hooks/hooks.toml, composes a provider-specific view, and writes the appropriate output files for each enabled provider.
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 shared top-level section plus optional provider-specific sections:
# Shared hooks[[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
# Provider-specific override for Codex only[[codex.PreToolUse]]matcher = "Bash"[[codex.PreToolUse.hooks]]type = "command"command = "${OMNIDEV_CAPABILITY_ROOT}/hooks/validate-bash.sh"statusMessage = "Checking Bash command"Provider composition rules:
- Top-level events are shared.
[claude]and[codex]are optional provider-specific escape hatches.- If a provider section defines an event, that event replaces the shared event for that provider only.
- Hooks that are not usable in the active provider are skipped with warnings instead of failing sync.
Provider Compatibility
Section titled “Provider Compatibility”| Provider | Output files | Notes |
|---|---|---|
| Claude Code | .claude/settings.json | Shared hooks plus [claude] overrides |
| Codex | .codex/hooks.json, .codex/config.toml | Shared hooks plus [codex] overrides; features.hooks = true is written automatically |
Shared hooks
Section titled “Shared hooks”Top-level hooks should be the portable/common subset you want available on both providers.
Provider-specific hooks
Section titled “Provider-specific hooks”Use provider sections when a hook only makes sense on one provider:
[[claude.PermissionRequest]]matcher = "Bash"[[claude.PermissionRequest.hooks]]type = "prompt"prompt = "Review this permission request."
[[codex.PreToolUse]]matcher = "Bash"[[codex.PreToolUse.hooks]]type = "command"command = "${OMNIDEV_CAPABILITY_ROOT}/hooks/pre-tool.sh"statusMessage = "Checking Bash command"The [claude] section accepts Claude-native events in addition to the shared top-level subset. This is where provider-specific events such as WorktreeCreate, WorktreeRemove, PermissionDenied, PostToolUseFailure, PostCompact, FileChanged, ConfigChange, and related Claude lifecycle hooks belong.
Hook Events
Section titled “Hook Events”OmniDev’s shared top-level format still supports the existing OmniDev hook events, but providers use only what they can materialize. Claude gets the full shared set plus [claude] overrides. Codex uses its current documented subset and warns when shared hooks cannot be used there.
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 | Yes |
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 | Yes | 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 | Yes | No |
PreCompact | Before context compaction | Yes | No |
Claude-only events in [claude]
Section titled “Claude-only events in [claude]”Use the [claude] section for Claude lifecycle hooks that are not part of the shared top-level subset:
PermissionDeniedPostToolUseFailureSubagentStartTaskCreatedTaskCompletedStopFailureTeammateIdleInstructionsLoadedConfigChangeCwdChangedFileChangedWorktreeCreateWorktreeRemovePostCompactElicitationElicitationResult
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 shared top-level hooks, UserPromptSubmit, Stop, and SubagentStop ignore matcher. Several Claude-only events in [claude] also ignore matchers, including TaskCreated, TaskCompleted, TeammateIdle, CwdChanged, WorktreeCreate, and WorktreeRemove.
[[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. Supported for the shared Claude-compatible subset: PreToolUse, PostToolUse, PermissionRequest, UserPromptSubmit, Stop, and SubagentStop. Some Claude-only events in [claude] also support prompts, such as PostToolUseFailure, TaskCreated, and TaskCompleted.
[[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 throughout hooks.toml, including provider-specific sections:
| In hooks.toml | Provider output | Description |
|---|---|---|
${OMNIDEV_CAPABILITY_ROOT} | Resolved provider command path | Capability root directory |
${OMNIDEV_PROJECT_DIR} | Resolved provider command path | Project root |
Always use OMNIDEV_ prefixed variables in your authored hooks.toml. OmniDev resolves them before writing provider output.
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 - Separates shared hooks from
[claude]and[codex]sections - Resolves
OMNIDEV_variables to provider-ready command paths - Builds a provider-specific view of the hooks
- Warns about hooks that are not usable in the active provider
- Writes
.claude/settings.jsonand/or.codex/hooks.json - Enables
features.hooksin.codex/config.tomlwhen Codex hooks are present
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 Claude specification
- Codex Hooks Documentation - Full Codex specification