Hook Creator
$ARGUMENTS
Create a new Claude Code hook following ai-toolkit conventions.
Supported Hook Events
Core lifecycle
| Event |
Fires When |
Matcher |
Typical Use |
SessionStart |
Session begins, resumes, or clears |
startup|resume|clear |
Context injection, rules reminder |
SessionEnd |
Session is closing |
any |
Flush logs, save transcripts |
UserPromptSubmit |
User submits a prompt |
any |
Prompt governance, usage tracking |
Notification |
Claude sends a notification |
any |
OS alerts, Slack pings |
MessageDisplay |
Assistant message text streams in completed-line batches |
none |
Return displayContent to replace the rendered batch without changing the transcript |
Tool lifecycle
| Event |
Fires When |
Matcher |
Typical Use |
PreToolUse |
Before a tool executes |
tool name (e.g. Bash) or if: rule |
Safety guards, validation, "defer" for headless |
PostToolUse |
After a tool executes |
tool name |
Feedback loops, logging, format-on-save |
PostToolUseFailure |
After a tool fails |
tool name |
Failure telemetry, recovery hints |
PostToolBatch |
After a batch of tool calls completes |
any |
Batch summaries, aggregate validation |
Turn lifecycle
| Event |
Fires When |
Matcher |
Typical Use |
Stop |
Claude finishes responding |
any |
Quality checks, session save |
StopFailure |
Turn ends due to an API error (rate limit, auth) |
any |
Alerting, fallback behavior |
UserPromptExpansion |
Claude expands or rewrites a submitted prompt |
any |
Prompt policy and context shaping |
Subagent lifecycle
| Event |
Fires When |
Matcher |
Typical Use |
SubagentStart |
Subagent launches |
any |
Observability |
SubagentStop |
Subagent completes |
any |
Result validation |
Compaction
| Event |
Fires When |
Matcher |
Typical Use |
PreCompact |
Before context compaction; can block with exit 2 or {"decision":"block"} |
any |
Context preservation |
PostCompact |
After compaction completes |
any |
Re-inject state that was summarized away |
Permissions & elicitation
| Event |
Fires When |
Matcher |
Typical Use |
PermissionRequest |
Tool awaiting permission; can return updatedInput |
any |
Headless approval flows |
PermissionDenied |
Auto-mode classifier denied a tool call; return {retry: true} to allow retry |
any |
Coach the model, log denials |
Elicitation |
MCP elicitation/create request arrives |
any |
Intercept / override MCP UI prompts |
ElicitationResult |
Elicitation response ready to be sent back |
any |
Validate / transform elicitation replies |
Agent Teams
| Event |
Fires When |
Matcher |
Typical Use |
TaskCreated |
New task registered via TaskCreate |
any |
Audit, assignment routing |
TaskCompleted |
Agent Teams task finished |
any |
Lint, type check, notify |
TeammateIdle |
Agent Teams member idle |
any |
Completeness reminder |
Worktrees & environment
| Event |
Fires When |
Matcher |
Typical Use |
WorktreeCreate |
Worktree is being created; type: "http" can return hookSpecificOutput.worktreePath |
any |
Provision worktree dirs |
WorktreeRemove |
Worktree is being removed |
any |
Cleanup |
CwdChanged |
Working directory changes during a session |
any |
Reactive env management (e.g., direnv) |
DirectoryAdded |
A working directory was added with /add-dir or SDK register_repo_root; runs asynchronously after the add and cannot block it |
slash_command|register_repo_root |
Prepare a newly added repository |
FileChanged |
Tracked file is modified on disk |
any |
Re-lint, reload config |
ConfigChange |
Settings / config file changed |
any |
Re-validate, warn on drift |
Setup / bootstrap
| Event |
Fires When |
Matcher |
Typical Use |
Setup |
First-run / initialization |
any |
Project bootstrap |
InstructionsLoaded |
CLAUDE.md / AGENTS.md loaded into context |
any |
Verify presence of mandatory rules |
Hook Handler Types
Claude Code supports five handler type values in hooks.json:
| Type |
Purpose |
Required fields |
command |
Run a shell script / binary |
command (path + args) |
http |
Call a local or remote HTTP endpoint |
url |
prompt |
Inject a prompt to the fast inline model and use its verdict |
prompt |
agent |
Spawn an experimental tool-using subagent to evaluate the event |
prompt |
mcp_tool |
Invoke an MCP tool directly (no subprocess) |
server, tool, arguments |
command remains the default and ai-toolkit's hook entries all use it. The other types are documented here so you can author them by hand when needed.
Workflow
- Capture intent -- ask: what should the hook do? Which lifecycle event?
- Select event -- pick from the Supported Hook Events table above
- Define matcher -- tool name for PreToolUse/PostToolUse, empty for global
- Write script -- create
app/hooks/{event-name-kebab}.sh
- Register in hooks.json -- add entry to
app/hooks.json
- Validate -- run
scripts/validate.py
Hook Script Conventions
- Location:
app/hooks/{event-name-kebab}.sh
- Shebang:
#!/bin/bash
- Header comment: script name, purpose, event, matcher
- Respect
TOOLKIT_HOOK_PROFILE env var (minimal = skip non-essential hooks)
- Always
exit 0 on success (non-zero blocks the operation for Pre* hooks)
- Output goes to Claude's context as plain text
- No external dependencies -- bash builtins and coreutils only
- Keep output concise -- hooks fire frequently
hooks.json Entry Format
{
"_source": "ai-toolkit",
"matcher": "",
"hooks": [
{
"type": "command",
"command": "\"$HOME/.softspark/ai-toolkit/hooks/{script-name}.sh\""
}
]
}
Required fields:
_source: always "ai-toolkit" (used by merge/strip logic)
matcher: tool name or regex for Pre/PostToolUse, empty string for global events
hooks[].type: "command", "http", "prompt", "agent", or "mcp_tool" (ai-toolkit uses "command")
hooks[].command: path to script using $HOME/.softspark/ai-toolkit/hooks/ prefix (for type: command)
Optional fields (read from Claude Code docs, not emitted by ai-toolkit by default):
Common to every handler type:
hooks[].if: one permission-rule filter (e.g. "Bash(git push*)"); evaluated only for tool events
hooks[].timeout: seconds to wait before canceling the handler (type and event defaults apply if omitted)
hooks[].statusMessage: short message surfaced in the UI while the hook runs
hooks[].once: run once per session; only honored in skill frontmatter and ignored in settings files or agent frontmatter
Command-handler fields:
hooks[].args: argument vector for exec form. Prefer exec form when a command uses ${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT}, or ${CLAUDE_PLUGIN_DATA} path placeholders
hooks[].async: run in the background without blocking; background hooks cannot return decisions
hooks[].asyncRewake: run in the background and wake Claude on exit code 2; implies async
hooks[].shell: choose bash or powershell for shell form; ignored when args selects exec form
Prompt and agent handlers both require hooks[].prompt; type: agent does not accept an agent name field. Agent handlers are experimental, so prefer command for production enforcement.
MessageDisplay has no matcher, runs once per rendered batch in interactive sessions, and defaults to a 10-second timeout. Its displayContent output changes only the screen text; Claude and the transcript retain the original response. DirectoryAdded is always asynchronous and non-blocking regardless of the handler configuration.
Script Template
#!/bin/bash
# {script-name}.sh — {One-line purpose}.
#
# Fires on: {EventName}
# Matcher: {matcher or "all"}
# Skipped when TOOLKIT_HOOK_PROFILE=minimal.
PROFILE="${TOOLKIT_HOOK_PROFILE:-standard}"
[ "$PROFILE" = "minimal" ] && exit 0
# --- Hook logic here ---
exit 0
Rules
- MUST use one script per hook entry — no inline multi-line commands inside
hooks.json
- MUST keep
Pre* hooks fast and deterministic — they gate every matching tool call, slow hooks throttle the whole agent
- NEVER write secrets, tokens, or credentials to stdout — hook output is injected into LLM context and can be extracted
- NEVER exit non-zero from a
Post* or Stop hook unless you intend to block further processing; exit 0 is the safe default
- CRITICAL: respect the
TOOLKIT_HOOK_PROFILE env var. Profile minimal must be a no-op for non-essential hooks.
- MANDATORY: test the script standalone (
bash app/hooks/{name}.sh) before adding it to hooks.json
Gotchas
PreToolUse hooks that exit non-zero block the tool call. A slow or flaky hook (network call, lock contention) becomes a DoS against Claude's own workflow. Keep Pre hooks to pure-bash checks of local state.
- Hook output (stdout) is injected verbatim into the model's context. A hook that runs
git log --all prints hundreds of lines the model then has to wade through — be surgical, print only what matters.
- The path in
hooks.json is resolved relative to the user's machine, not the ai-toolkit repo. Use $HOME/.softspark/ai-toolkit/hooks/<name>.sh as the canonical location (installer symlinks there).
SessionStart with matcher startup|compact fires on both fresh starts AND after context compaction. Hooks that assume "new session" will mis-fire after compaction — check for explicit context markers if the distinction matters.
- Bash hooks on Windows (without WSL) will not run. If the hook must work cross-platform, wrap it in a Node or Python script and call from the bash stub — or flag the hook as
posix-only in the description.
Validation Checklist
After creating the hook:
When NOT to Use
- For a skill (slash command) — use
/skill-creator
- For an agent definition — use
/agent-creator
- For a git pre-commit hook (not a Claude Code hook) — use
/git-mastery or scripts/install_git_hooks.py
- For one-off automation that is not tied to a Claude Code event — use a plain shell script outside the toolkit
- To modify an existing toolkit hook — edit the file directly; this skill is create-only
1---2name: hook-creator3description: Create new Claude Code lifecycle hook (PreToolUse/PostToolUse/Stop/SessionStart) with bash + hooks.json. Triggers: create hook, lifecycle hook, PreToolUse, PostToolUse, hook event.4---56# Hook Creator78$ARGUMENTS910Create a new Claude Code hook following ai-toolkit conventions.1112## Supported Hook Events1314### Core lifecycle1516| Event | Fires When | Matcher | Typical Use |17|-------|-----------|---------|-------------|18| `SessionStart` | Session begins, resumes, or clears | `startup\|resume\|clear` | Context injection, rules reminder |19| `SessionEnd` | Session is closing | any | Flush logs, save transcripts |20| `UserPromptSubmit` | User submits a prompt | any | Prompt governance, usage tracking |21| `Notification` | Claude sends a notification | any | OS alerts, Slack pings |22| `MessageDisplay` | Assistant message text streams in completed-line batches | none | Return `displayContent` to replace the rendered batch without changing the transcript |2324### Tool lifecycle2526| Event | Fires When | Matcher | Typical Use |27|-------|-----------|---------|-------------|28| `PreToolUse` | Before a tool executes | tool name (e.g. `Bash`) or `if:` rule | Safety guards, validation, `"defer"` for headless |29| `PostToolUse` | After a tool executes | tool name | Feedback loops, logging, format-on-save |30| `PostToolUseFailure` | After a tool fails | tool name | Failure telemetry, recovery hints |31| `PostToolBatch` | After a batch of tool calls completes | any | Batch summaries, aggregate validation |3233### Turn lifecycle3435| Event | Fires When | Matcher | Typical Use |36|-------|-----------|---------|-------------|37| `Stop` | Claude finishes responding | any | Quality checks, session save |38| `StopFailure` | Turn ends due to an API error (rate limit, auth) | any | Alerting, fallback behavior |39| `UserPromptExpansion` | Claude expands or rewrites a submitted prompt | any | Prompt policy and context shaping |4041### Subagent lifecycle4243| Event | Fires When | Matcher | Typical Use |44|-------|-----------|---------|-------------|45| `SubagentStart` | Subagent launches | any | Observability |46| `SubagentStop` | Subagent completes | any | Result validation |4748### Compaction4950| Event | Fires When | Matcher | Typical Use |51|-------|-----------|---------|-------------|52| `PreCompact` | Before context compaction; can block with exit 2 or `{"decision":"block"}` | any | Context preservation |53| `PostCompact` | After compaction completes | any | Re-inject state that was summarized away |5455### Permissions & elicitation5657| Event | Fires When | Matcher | Typical Use |58|-------|-----------|---------|-------------|59| `PermissionRequest` | Tool awaiting permission; can return `updatedInput` | any | Headless approval flows |60| `PermissionDenied` | Auto-mode classifier denied a tool call; return `{retry: true}` to allow retry | any | Coach the model, log denials |61| `Elicitation` | MCP `elicitation/create` request arrives | any | Intercept / override MCP UI prompts |62| `ElicitationResult` | Elicitation response ready to be sent back | any | Validate / transform elicitation replies |6364### Agent Teams6566| Event | Fires When | Matcher | Typical Use |67|-------|-----------|---------|-------------|68| `TaskCreated` | New task registered via `TaskCreate` | any | Audit, assignment routing |69| `TaskCompleted` | Agent Teams task finished | any | Lint, type check, notify |70| `TeammateIdle` | Agent Teams member idle | any | Completeness reminder |7172### Worktrees & environment7374| Event | Fires When | Matcher | Typical Use |75|-------|-----------|---------|-------------|76| `WorktreeCreate` | Worktree is being created; `type: "http"` can return `hookSpecificOutput.worktreePath` | any | Provision worktree dirs |77| `WorktreeRemove` | Worktree is being removed | any | Cleanup |78| `CwdChanged` | Working directory changes during a session | any | Reactive env management (e.g., direnv) |79| `DirectoryAdded` | A working directory was added with `/add-dir` or SDK `register_repo_root`; runs asynchronously after the add and cannot block it | `slash_command\|register_repo_root` | Prepare a newly added repository |80| `FileChanged` | Tracked file is modified on disk | any | Re-lint, reload config |81| `ConfigChange` | Settings / config file changed | any | Re-validate, warn on drift |8283### Setup / bootstrap8485| Event | Fires When | Matcher | Typical Use |86|-------|-----------|---------|-------------|87| `Setup` | First-run / initialization | any | Project bootstrap |88| `InstructionsLoaded` | CLAUDE.md / AGENTS.md loaded into context | any | Verify presence of mandatory rules |8990## Hook Handler Types9192Claude Code supports five handler `type` values in `hooks.json`:9394| Type | Purpose | Required fields |95|------|---------|-----------------|96| `command` | Run a shell script / binary | `command` (path + args) |97| `http` | Call a local or remote HTTP endpoint | `url` |98| `prompt` | Inject a prompt to the fast inline model and use its verdict | `prompt` |99| `agent` | Spawn an experimental tool-using subagent to evaluate the event | `prompt` |100| `mcp_tool` | Invoke an MCP tool directly (no subprocess) | `server`, `tool`, `arguments` |101102`command` remains the default and ai-toolkit's hook entries all use it. The other types are documented here so you can author them by hand when needed.103104## Workflow1051061. **Capture intent** -- ask: what should the hook do? Which lifecycle event?1072. **Select event** -- pick from the Supported Hook Events table above1083. **Define matcher** -- tool name for PreToolUse/PostToolUse, empty for global1094. **Write script** -- create `app/hooks/{event-name-kebab}.sh`1105. **Register in hooks.json** -- add entry to `app/hooks.json`1116. **Validate** -- run `scripts/validate.py`112113## Hook Script Conventions114115- Location: `app/hooks/{event-name-kebab}.sh`116- Shebang: `#!/bin/bash`117- Header comment: script name, purpose, event, matcher118- Respect `TOOLKIT_HOOK_PROFILE` env var (`minimal` = skip non-essential hooks)119- Always `exit 0` on success (non-zero blocks the operation for Pre* hooks)120- Output goes to Claude's context as plain text121- No external dependencies -- bash builtins and coreutils only122- Keep output concise -- hooks fire frequently123124## hooks.json Entry Format125126```json127{128 "_source": "ai-toolkit",129 "matcher": "",130 "hooks": [131 {132 "type": "command",133 "command": "\"$HOME/.softspark/ai-toolkit/hooks/{script-name}.sh\""134 }135 ]136}137```138139Required fields:140- `_source`: always `"ai-toolkit"` (used by merge/strip logic)141- `matcher`: tool name or regex for Pre/PostToolUse, empty string for global events142- `hooks[].type`: `"command"`, `"http"`, `"prompt"`, `"agent"`, or `"mcp_tool"` (ai-toolkit uses `"command"`)143- `hooks[].command`: path to script using `$HOME/.softspark/ai-toolkit/hooks/` prefix (for `type: command`)144145Optional fields (read from Claude Code docs, not emitted by ai-toolkit by default):146147Common to every handler type:148- `hooks[].if`: one permission-rule filter (e.g. `"Bash(git push*)"`); evaluated only for tool events149- `hooks[].timeout`: seconds to wait before canceling the handler (type and event defaults apply if omitted)150- `hooks[].statusMessage`: short message surfaced in the UI while the hook runs151- `hooks[].once`: run once per session; only honored in skill frontmatter and ignored in settings files or agent frontmatter152153Command-handler fields:154- `hooks[].args`: argument vector for exec form. Prefer exec form when a command uses `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_PLUGIN_ROOT}`, or `${CLAUDE_PLUGIN_DATA}` path placeholders155- `hooks[].async`: run in the background without blocking; background hooks cannot return decisions156- `hooks[].asyncRewake`: run in the background and wake Claude on exit code 2; implies `async`157- `hooks[].shell`: choose `bash` or `powershell` for shell form; ignored when `args` selects exec form158159Prompt and agent handlers both require `hooks[].prompt`; `type: agent` does not accept an agent name field. Agent handlers are experimental, so prefer `command` for production enforcement.160161`MessageDisplay` has no matcher, runs once per rendered batch in interactive sessions, and defaults to a 10-second timeout. Its `displayContent` output changes only the screen text; Claude and the transcript retain the original response. `DirectoryAdded` is always asynchronous and non-blocking regardless of the handler configuration.162163## Script Template164165```bash166#!/bin/bash167# {script-name}.sh — {One-line purpose}.168#169# Fires on: {EventName}170# Matcher: {matcher or "all"}171# Skipped when TOOLKIT_HOOK_PROFILE=minimal.172173PROFILE="${TOOLKIT_HOOK_PROFILE:-standard}"174[ "$PROFILE" = "minimal" ] && exit 0175176# --- Hook logic here ---177178exit 0179```180181## Rules182183- **MUST** use one script per hook entry — no inline multi-line commands inside `hooks.json`184- **MUST** keep `Pre*` hooks fast and deterministic — they gate every matching tool call, slow hooks throttle the whole agent185- **NEVER** write secrets, tokens, or credentials to stdout — hook output is injected into LLM context and can be extracted186- **NEVER** exit non-zero from a `Post*` or `Stop` hook unless you intend to block further processing; exit 0 is the safe default187- **CRITICAL**: respect the `TOOLKIT_HOOK_PROFILE` env var. Profile `minimal` must be a no-op for non-essential hooks.188- **MANDATORY**: test the script standalone (`bash app/hooks/{name}.sh`) before adding it to `hooks.json`189190## Gotchas191192- `PreToolUse` hooks that exit non-zero **block** the tool call. A slow or flaky hook (network call, lock contention) becomes a DoS against Claude's own workflow. Keep Pre hooks to pure-bash checks of local state.193- Hook output (stdout) is injected verbatim into the model's context. A hook that runs `git log --all` prints hundreds of lines the model then has to wade through — be surgical, print only what matters.194- The path in `hooks.json` is resolved relative to the user's machine, not the ai-toolkit repo. Use `$HOME/.softspark/ai-toolkit/hooks/<name>.sh` as the canonical location (installer symlinks there).195- `SessionStart` with matcher `startup|compact` fires on both fresh starts AND after context compaction. Hooks that assume "new session" will mis-fire after compaction — check for explicit context markers if the distinction matters.196- Bash hooks on Windows (without WSL) will not run. If the hook must work cross-platform, wrap it in a Node or Python script and call from the bash stub — or flag the hook as `posix-only` in the description.197198## Validation Checklist199200After creating the hook:201202- [ ] Script exists in `app/hooks/` and is executable (`chmod +x`)203- [ ] Entry added to `app/hooks.json` with `_source: "ai-toolkit"`204- [ ] Event name matches a supported lifecycle event205- [ ] `scripts/validate.py` passes206- [ ] Script runs without errors: `bash app/hooks/{name}.sh`207- [ ] Hook count in README.md and docs updated if needed208209## When NOT to Use210211- For a **skill** (slash command) — use `/skill-creator`212- For an **agent** definition — use `/agent-creator`213- For a git pre-commit hook (not a Claude Code hook) — use `/git-mastery` or `scripts/install_git_hooks.py`214- For one-off automation that is not tied to a Claude Code event — use a plain shell script outside the toolkit215- To modify an existing toolkit hook — edit the file directly; this skill is create-only