Hook / Plugin Creator
Overview
This skill helps design and implement event hooks (or plugins). Hooks allow intercepting agent actions (command execution, file read/write, tool usage) and applying rules: allow, block (with explanation), request confirmation, or modify input data.
IDE Detection and Hook Placement
Hook Paths by IDE
| IDE | User Hooks | Project Hooks | Format |
|---|---|---|---|
| Cursor | ~/.cursor/hooks.json |
.cursor/hooks.json |
JSON config + shell scripts |
| Claude Code | ~/.claude/settings.json |
.claude/settings.json.claude/settings.local.json |
JSON config (4 hook types) |
| OpenCode | ~/.config/opencode/plugins/*.{js,ts} |
.opencode/plugins/*.{js,ts}opencode.json plugin (npm) |
JS/TS modules (event-based) |
Detection Strategy
- Check for IDE-specific markers:
.cursor/directory → Cursor.claude/directory → Claude Code.opencode/directory oropencode.json→ OpenCode
- If multiple detected or none → ask the user
IMPORTANT: After detecting the IDE, read ONLY the corresponding reference file. Do not load all references.
Hook Type Availability Matrix
| Type | Cursor | Claude Code | OpenCode |
|---|---|---|---|
| Command (shell script) | Yes | Yes | No (use plugin code) |
| Prompt (LLM-evaluated) | Yes | Yes (8 events) | No |
| HTTP (POST to URL) | No | Yes | No |
| Agent (multi-turn subagent) | No | Yes (8 events) | No |
| Programmatic (JS/TS code) | No | No | Yes |
Cross-IDE Event Mapping
| Cursor Event | Claude Code Event | OpenCode Equivalent |
|---|---|---|
beforeShellExecution |
PreToolUse (matcher: "Bash") |
tool.execute.before |
afterShellExecution |
PostToolUse (matcher: "Bash") |
tool.execute.after |
preToolUse |
PreToolUse |
tool.execute.before |
postToolUse |
PostToolUse |
tool.execute.after |
postToolUseFailure |
PostToolUseFailure |
event → tool.execute.after |
sessionStart |
SessionStart |
event → session.created |
sessionEnd |
SessionEnd |
event → session.deleted |
subagentStart |
SubagentStart |
— |
subagentStop |
SubagentStop |
— |
stop |
Stop |
event → session.idle |
preCompact |
PreCompact |
experimental.session.compacting |
afterFileEdit |
PostToolUse (matcher: "Edit|Write") |
event → file.edited |
beforeSubmitPrompt |
UserPromptSubmit |
chat.message |
beforeMCPExecution |
PreToolUse (matcher: "mcp__.*") |
tool.execute.before |
afterMCPExecution |
PostToolUse (matcher: "mcp__.*") |
tool.execute.after |
beforeReadFile |
PreToolUse (matcher: "Read") |
tool.execute.before |
| — | PermissionRequest |
permission.ask |
| — | Notification |
event → various |
| — | TeammateIdle |
— |
| — | TaskCompleted |
event → todo.updated |
| — | ConfigChange |
— |
| — | InstructionsLoaded |
— |
| — | WorktreeCreate / WorktreeRemove |
— |
Main Workflow
- Detect IDE using markers above
- Read the IDE-specific reference (see Resources below)
- Define the Event: Choose from the mapping table above
- Choose Implementation Type per availability matrix
- Configure: Add config to the correct location
- Implement Logic: Create the script/plugin
- Test: Verify the hook fires correctly
Cursor Hooks
JSON config + shell scripts. Two types: command and prompt.
Configuration
In .cursor/hooks.json:
{
"version": 1,
"hooks": {
"afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }],
"beforeShellExecution": [
{ "command": ".cursor/hooks/guard.sh", "matcher": "rm " }
]
}
}
Implementation Types
- Command-based: Shell script receiving JSON via stdin, returning JSON via stdout. Exit 0 = success, 2 = deny.
- Prompt-based: LLM-evaluated condition.
{ "type": "prompt", "prompt": "Is this command safe?", "timeout": 10 }
Full Reference
For events list, input/output schemas, env vars, matcher values: see hooks_api.md.
Example: Blocking Dangerous Commands
guard.sh:
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.command')
if [[ "$command" == *"rm -rf"* ]]; then
echo '{"permission": "ask", "user_message": "Are you sure?", "agent_message": "rm -rf requires confirmation."}'
else
echo '{"permission": "allow"}'
fi
Claude Code Hooks
JSON config in settings.json. Four types: command, http, prompt, agent.
Configuration
In .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/guard.sh",
"timeout": 30
}
]
}
]
}
}
Note the nested structure: each event maps to an array of matcher groups, each containing a hooks array.
Implementation Types
- Command: Shell script. Exit 0 = allow, exit 2 = block (stderr = reason).
- HTTP: POST event data to URL. Response body parsed as JSON.
- Prompt: Single-turn LLM evaluation with
$ARGUMENTSplaceholder. - Agent: Multi-turn subagent verification with tool access (up to 50 turns).
Prompt/agent types only available for 8 events: PermissionRequest, PostToolUse, PostToolUseFailure, PreToolUse, Stop, SubagentStop, TaskCompleted, UserPromptSubmit.
Full Reference
For all 18 events, input/output schemas, env vars, exit code behavior per event: see claude_code_hooks_api.md.
Example: Blocking Dangerous Commands
guard.sh (same script works for both Cursor and Claude Code):
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
tool_input=$(echo "$input" | jq -r '.tool_input.command // empty')
if [[ "$tool_input" == *"rm -rf"* ]]; then
echo "Blocked: rm -rf requires manual confirmation" >&2
exit 2
fi
exit 0
OpenCode Plugins
JS/TS modules returning a Hooks object. No JSON config — all logic is code.
Plugin Structure
import type { Plugin } from "@opencode-ai/plugin"
export default (async ({ project, client, $, directory, worktree }) => {
return {
"tool.execute.before": async (input) => {
// inspect/modify tool arguments
return input
},
event: async (event) => {
// handle any system event
},
}
}) satisfies Plugin
Distribution
- Local:
.opencode/plugins/*.ts - npm:
opencode.json→"plugin": ["package-name"]
Full Reference
For all hooks, events, tool() helper, examples: see opencode_plugins_api.md.
Example: Blocking Dangerous Commands
import type { Plugin } from "@opencode-ai/plugin"
export default (async ({ client }) => ({
"tool.execute.before": async (input) => {
if (input.tool === "bash" && input.args?.command?.includes("rm -rf")) {
client.app.log({ body: { level: "warn", message: "Blocked rm -rf" } })
return { ...input, args: { command: "echo 'Blocked: rm -rf not allowed'" } }
}
return input
},
})) satisfies Plugin
Resources
- hooks_api.md — Cursor: full event list, I/O formats, env vars
- claude_code_hooks_api.md — Claude Code: 18 events, 4 hook types, settings.json format
- opencode_plugins_api.md — OpenCode: plugin API, hooks, events, tool() helper
assets/hook_template.sh— Bash script template for command-type hooks
Tips
- All IDEs: Paths in hook configs are relative to project root
- Cursor: Use
matcherso hooks only fire for relevant commands - Cursor: Debug via "Hooks" tab in settings or "Hooks" output channel
- Cursor: Set
failClosed: truefor security-critical hooks - Claude Code: Use
claude --debugorCtrl+Ofor verbose hook logging - Claude Code: Interactive
/hooksmenu available for managing hooks - Claude Code: Check
stop_hook_activein Stop hooks to prevent infinite loops - Claude Code:
PermissionRequesthooks don't fire in non-interactive mode (-p) - OpenCode: Plugin dependencies go in
.opencode/package.json(Bun auto-installs) - OpenCode: Plugin tools override built-in tools with matching names