- name
- hooks-management
- description
- Manage hooks and automation for coding agents (Claude Code, Codex CLI, OpenCode, Devin CLI/Desktop). Use when users want to add, list, remove, update, or validate hooks. Triggers on requests like "add a hook", "create a hook that...", "list my hooks", "remove the hook", "validate hooks", or any mention of automating agent behavior with shell commands or plugins.
# Hooks Management
Manage hooks and automation through natural language commands.
**IMPORTANT**: After adding, modifying, or removing hooks, always inform the user that they need to **restart the agent** for changes to take effect. Hooks are loaded at startup.
## Quick Reference
**Hook Events** (Claude Code, as of 2026-04 — 28 events):
- *Session lifecycle*: SessionStart, SessionEnd, InstructionsLoaded
- *User input*: UserPromptSubmit, UserPromptExpansion
- *Tool execution*: PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch
- *Permissions*: PermissionRequest, PermissionDenied
- *Model output*: Stop, StopFailure
- *Subagents/tasks*: SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle
- *Config/state*: ConfigChange, FileChanged, CwdChanged
- *Compaction*: PreCompact, PostCompact
- *Worktree*: WorktreeCreate, WorktreeRemove
- *MCP*: Elicitation, ElicitationResult
- *Notifications*: Notification
**Handler types**: `command`, `http`, `mcp_tool`, `prompt`, `agent`. Some events are command-only (PostCompact, PermissionDenied, Elicitation/ElicitationResult, FileChanged, CwdChanged, ConfigChange, InstructionsLoaded, WorktreeCreate/Remove, SubagentStart, StopFailure, TeammateIdle, Setup, SessionStart, SessionEnd, Notification).
**Claude Code Settings Files**:
- User-wide: `~/.claude/settings.json`
- Project: `.claude/settings.json`
- Local (not committed): `.claude/settings.local.json`
- Drop-in policy fragments: `~/.claude/managed-settings.d/` (managed-settings only)
**Codex CLI / Codex App Settings Files (current as of 2026-06)**:
- User config: `~/.codex/config.toml`
- User hooks: `~/.codex/hooks.json` or inline `[hooks]` tables in `~/.codex/config.toml`
- Project hooks: `<repo>/.codex/hooks.json` or inline `[hooks]` tables in `<repo>/.codex/config.toml` (trusted projects only)
- Codex App and CLI share these config layers. In the App/IDE, the settings UI opens the same `config.toml`.
- Hooks are enabled by default. Use `[features].hooks = false` to disable them. `codex_hooks` is a deprecated alias.
- Non-managed Codex command hooks must be reviewed/trusted with `/hooks`; changed hook definitions are skipped until trusted.
**Devin CLI / Desktop hook locations**:
- Project: `.devin/hooks.v1.json` (standalone hooks object, recommended) or `"hooks"` in `.devin/config.json` / `.devin/config.local.json`
- User: `"hooks"` in `~/.config/devin/config.json` (`%APPDATA%\devin\config.json` on Windows)
- Claude-format hooks under `.claude/` are imported automatically when `read_config_from.claude` is on (default)
- Events: PreToolUse, PostToolUse, PermissionRequest, UserPromptSubmit, Stop, PostCompaction, SessionStart, SessionEnd; `matcher` is a regex on `tool_name`
- See [references/devin-hooks.md](references/devin-hooks.md) for the full event/output contract
**Claude Code default control mechanism for PreToolUse**: emit JSON on stdout with `hookSpecificOutput.permissionDecision` set to `"allow"`, `"deny"`, **`"ask"`** (triggers the built-in user confirmation prompt), or **`"defer"`** (pause headless tool calls; resume with `-p --resume`). See [Decision Control](#decision-control-pretooluse). Do NOT roll your own confirmation schemes (env-var flags, interactive `osascript` prompts, bypass tokens) — those break the built-in UX and silently fail under existing `permissions.allow` entries.
**Codex exception**: Codex `PreToolUse` does not support `"ask"` yet. In Codex configs, use `deny` / exit code 2 for hard blocks, `additionalContext` for advisory context, or Codex approval policy/permissions for native prompts.
**Disable all hooks**: set `disableAllHooks: true` in settings.json.
## Workflow
### 1. Understand the Request
Parse what the user wants:
- **Add/Create**: New hook for specific event and tool
- **List/Show**: Display current hooks configuration
- **Remove/Delete**: Remove specific hook(s)
- **Update/Modify**: Change existing hook
- **Validate**: Check hooks for errors
### 2. Validate Before Writing
Always run validation before saving:
```bash
python3 "$SKILL_PATH/scripts/validate_hooks.py" ~/.claude/settings.json
```
### 3. Read Current Configuration
```bash
cat ~/.claude/settings.json 2>/dev/null || echo '{}'
```
### 4. Apply Changes
Use Edit tool for modifications, Write tool for new files.
## Adding Hooks
### Translate Natural Language to Hook Config
| User Says | Event | Matcher | Notes |
|-----------|-------|---------|-------|
| "log all bash commands" | PreToolUse | Bash | Logging to file |
| "format files after edit" | PostToolUse | Edit\|Write | Run formatter |
| "block .env file changes" | PreToolUse | Edit\|Write | Exit code 2 blocks |
| "notify me when done" | Notification | "" | Desktop notification |
| "run tests after code changes" | PostToolUse | Edit\|Write | Filter by extension |
| "ask before dangerous commands" | PreToolUse | Bash | Claude Code: emit JSON `permissionDecision: "ask"` (built-in confirm UI). Codex: use approval policy if possible; hook-level `ask` is unsupported. |
| "require manual approval for X" | PreToolUse | Bash/Edit/Write | Claude Code: emit JSON `permissionDecision: "ask"`, NOT exit 2. Codex: choose policy prompt or hard block. |
| "block unless confirmed" | PreToolUse | Bash | Claude Code: JSON `"ask"` lets the user approve per call. Codex: no hook-created confirmation prompt yet. |
### Hook Configuration Template
```json
{
"hooks": {
"EVENT_NAME": [
{
"matcher": "TOOL_PATTERN",
"hooks": [
{
"type": "command",
"command": "SHELL_COMMAND",
"timeout": 60
}
]
}
]
}
}
```
### Simple vs Complex Hooks
**PREFER SCRIPT FILES** for complex hooks. Inline commands with nested quotes, `osascript`, or multi-step logic often break due to JSON escaping issues.
| Complexity | Approach | Example |
|------------|----------|---------|
| Simple | Inline | `jq -r '.tool_input.command' >> log.txt` |
| Medium | Inline | Single grep/jq pipe with basic conditionals |
| Complex | **Script file** | Dialogs, multiple conditions, osascript, error handling |
**Script location**: `~/.claude/hooks/` (create if needed)
**Script template for PreToolUse** (`~/.claude/hooks/my-hook.sh`) — use JSON decision control as the primary mechanism; exit codes are a fallback for simple blocking only:
```bash
#!/bin/bash
set -euo pipefail
# Read JSON input from stdin
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
# Your logic here
if echo "$cmd" | grep -q 'pattern-requiring-confirmation'; then
# PRIMARY PATTERN for "require user confirmation": emit JSON on stdout.
# Claude Code will show its built-in confirm prompt to the user.
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: "Explain why this call is risky"
}
}'
exit 0
fi
if echo "$cmd" | grep -q 'pattern-to-hard-block'; then
# Hard block (no user override possible): JSON deny, NOT exit 2.
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Reason shown to Claude"
}
}'
exit 0
fi
exit 0 # Allow (silent)
```
**Why JSON decisions, not exit 2 or home-grown prompts:**
- `permissionDecision: "ask"` triggers the built-in Claude Code confirm UI — the user sees a clean prompt and can allow/deny per-call.
- `exit 2` is a blunt block; the user cannot override it from the UI, and Claude often re-tries with workarounds.
- Home-grown schemes (env-var flags like `CONFIRMED=1`, `osascript` dialogs, bypass tokens) break the native UX, leak into command history, and are silently bypassed if the tool already has a matching `permissions.allow` rule.
**Hook config using script**:
```json
{
"type": "command",
"command": "~/.claude/hooks/my-hook.sh"
}
```
**Other handler types (2026)**: `http` (POSTs event JSON to a URL), `mcp_tool` (calls a tool on a configured MCP server), `prompt` (evaluates a prompt with an LLM, supports `$ARGUMENTS`), `agent` (runs an agentic verifier with tools). Some events are command-only (PostCompact, PermissionDenied, Elicitation/ElicitationResult, FileChanged, CwdChanged, ConfigChange, InstructionsLoaded, WorktreeCreate/Remove, SubagentStart, StopFailure, TeammateIdle, SessionStart/End, Notification).
**Always**:
1. Create script in `~/.claude/hooks/`
2. Make executable: `chmod +x ~/.claude/hooks/my-hook.sh`
3. Test with sample input: `echo '{"tool_input":{"command":"test"}}' | ~/.claude/hooks/my-hook.sh`
### Common Patterns
**Logging (PreToolUse)**:
```json
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
}]
}
```
**File Protection (PreToolUse, exit 2 to block)**:
```json
{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.file_path' | grep -qE '(\\.env|secrets)' && exit 2 || exit 0"
}]
}
```
**Auto-format (PostToolUse)**:
```json
{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "file=$(jq -r '.tool_input.file_path'); [[ $file == *.ts ]] && npx prettier --write \"$file\" || true"
}]
}
```
**Desktop Notification (Notification)**:
```json
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs attention\" with title \"Claude Code\"'"
}]
}
```
## Decision Control (Claude Code PreToolUse)
Claude Code PreToolUse hooks control tool execution by emitting JSON on stdout. This is the **default mechanism** — use it instead of exit codes whenever the intent is richer than "silently allow / hard block", especially when the user should be asked to confirm.
| `permissionDecision` | Behavior | Use for |
|----------------------|----------|---------|
| `"allow"` | Bypass permissions, proceed silently | Pre-approving a safe call |
| `"deny"` | Block, reason shown to Claude | Hard block (no user override) |
| `"ask"` | **Built-in Claude Code confirm UI** shown to user | "Require manual approval for X" — the canonical pattern |
| `"defer"` | Pause headless tool call, resume via `-p --resume` | External-system integrations in headless (`-p`) sessions |
Additional JSON fields:
- `permissionDecisionReason` — shown to the user for `"allow"`/`"ask"`, shown to Claude for `"deny"`
- `updatedInput` — modify tool input before execution
- `additionalContext` — inject context for Claude before the tool executes
### Ask user before dangerous command (the canonical pattern)
When the user says anything like **"require manual confirmation"**, **"ask before doing X"**, **"don't run Y without my approval"** — this is the pattern. Do not invent bypass env vars, `osascript` dialogs, or confirmation tokens. The built-in prompt already handles per-call allow/deny and is the only path that integrates with existing `permissions.allow` rules correctly.
```bash
#!/bin/bash
set -euo pipefail
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'supabase\s+db\s+reset'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: "This will destroy and recreate the local database."
}
}'
else
exit 0
fi
```
### Deny with reason (hard block)
```bash
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive command blocked by hook"
}
}'
```
### Gotcha: `"ask"` vs existing `permissions.allow` rules
View on GitHub