A Claude Code plugin for declarative hook rules. Define behavior as YAML — hooksmith evaluates rules at runtime. No build step.
~/.config/hooksmith/hooks/*.yaml ──→ hooksmith eval ──→ JSON decision
One universal evaluator routes to the right rules at runtime. Claude Code fires an event, hooksmith checks matching rules, the first rule that triggers emits a decision.
| Mechanism | How it works | Best for |
|---|---|---|
match |
Tests a field against a POSIX ERE pattern | Simple pattern matching, zero overhead |
run |
Executes a bash script (inline or file) | Complex logic, stateful checks |
prompt |
Injects a prompt for Claude to reason about | Nuanced, context-dependent policies |
Requires Claude Code v2.1.91+.
claude plugin add ugudlado/hooksmithOr install locally:
git clone https://github.com/ugudlado/hooksmith.git
claude --plugin-dir /path/to/hooksmithDrop a YAML file and it's live next session:
# ~/.config/hooksmith/hooks/block-rm.yaml
rules:
- name: block-rm
on: PreToolUse Bash
match: command =~ rm[[:space:]]+-rf[[:space:]]+(/|~|\$HOME)
deny: "Blocked: destructive rm targeting system or home directory."No build step needed. On SessionStart, hooksmith scans all rules, rebuilds its routing index, and registers the right events automatically.
If you have hooks in settings.json, convert them to YAML:
# Preview what will be converted (dry-run)
hooksmith convert
# Write the YAML rule files
hooksmith convert --applyThen remove the converted entries from settings.json — hooksmith owns them now.
Ready-to-use rules you can copy to ~/.config/hooksmith/hooks/. Each rule is a standalone YAML file — pick what you need.
Block dangerous bash commands — catches rm -rf /, sudo, chmod 777, curl-pipe-sh:
# ~/.config/hooksmith/hooks/bash-safety-guard.yaml
rules:
- name: bash-safety-guard
on: PreToolUse Bash
run: ~/.config/hooksmith/scripts/bash-safety-guard.sh
deny: trueProcess kill guard — only allows killing processes Claude started or processes inside the current repo:
# ~/.config/hooksmith/hooks/process-kill-guard.yaml
rules:
- name: process-kill-guard
on: PreToolUse Bash
run: ~/.config/hooksmith/scripts/process-kill-guard.sh
deny: trueProtected files — asks for confirmation before editing lock files and manifests:
# ~/.config/hooksmith/hooks/protected-files.yaml
rules:
- name: protected-files
on: PreToolUse Write|Edit
run: ~/.config/hooksmith/scripts/protected-files.sh
ask: trueWorktree boundary — prevents writes outside the active git worktree:
# ~/.config/hooksmith/hooks/worktree-boundary.yaml
rules:
- name: worktree-boundary
on: PreToolUse Write|Edit
run: ~/.config/hooksmith/scripts/worktree-boundary.sh
deny: trueAutopilot redirect — detects feature/bug requests and suggests /develop:
# ~/.config/hooksmith/hooks/autopilot-redirect.yaml
rules:
- name: autopilot-redirect
on: UserPromptSubmit
prompt: |
Analyze this user message: $USER_PROMPT
If it's clearly a FEATURE or BUG request, respond with a workflow hint.
Otherwise respond with {}.
context: trueLoop detector — blocks Claude from getting stuck in retry loops:
# ~/.config/hooksmith/hooks/loop-detector.yaml
rules:
- name: loop-detector
on: Stop
run: ~/.config/hooksmith/scripts/loop-detector.sh
deny: trueGit status at session start — gives Claude branch and change awareness:
# ~/.config/hooksmith/hooks/session-git-status.yaml
rules:
- name: session-git-status
on: SessionStart
run: ~/.config/hooksmith/scripts/session-git-status.sh
context: truePost-compact reminders — re-injects critical context after compaction:
# ~/.config/hooksmith/hooks/post-compact-reminders.yaml
rules:
- name: post-compact-reminders
on: PostCompact
run: ~/.config/hooksmith/scripts/post-compact-reminders.sh
context: trueAuto-format — runs prettier/formatter after Write/Edit:
# ~/.config/hooksmith/hooks/auto-format.yaml
rules:
- name: auto-format
on: PostToolUse Write|Edit
run: ~/.config/hooksmith/scripts/auto-format.sh
context: trueSmart notifications — macOS alerts for permission prompts and idle:
# ~/.config/hooksmith/hooks/smart-notify.yaml
rules:
- name: smart-notify
on: Notification
run: ~/.config/hooksmith/scripts/smart-notify.sh
context: trueScripts referenced above live in
~/.config/hooksmith/scripts/. Seeexamples/for self-contained rules that don't need external scripts.
Packs are curated rule collections you can install from any git repo and update independently.
# From a git repo (whole repo as a pack)
hooksmith pack install owner/repo
# From a subdirectory of a git repo
hooksmith pack install ugudlado/hooksmith/packs/starter
# Full URL
hooksmith pack install https://github.com/owner/repohooksmith pack list # Show installed packs
hooksmith pack update [name] # Update one or all packs
hooksmith pack remove <name> # Remove an installed packPack rules have the lowest precedence. Override any pack rule by creating a rule with the same name at user or project level:
# Override: change bash-safety-guard from deny to ask
rules:
- name: bash-safety-guard
on: PreToolUse Bash
run: scripts/bash-safety-guard.sh
ask: true # ask instead of denyTo disable a pack rule entirely for a specific project:
# .hooksmith/hooks/overrides.yaml
rules:
- name: bash-safety-guard
on: PreToolUse Bash
match: command =~ .
deny: "unused"
enabled: falseThe hooksmith repo includes a starter pack with safety rules:
hooksmith pack install ugudlado/hooksmith/packs/starterIncludes: bash-safety-guard, process-kill-guard, protected-files, worktree-boundary.
The plugin's bin/ directory is added to PATH automatically — all commands are bare:
hooksmith list [--json] [--scope user|project|all] # Show registered rules
hooksmith init # Regenerate hooks.json from rules
hooksmith convert [--apply] [--scope user|project] # Migrate settings.json hooks to YAML
hooksmith pack <install|update|remove|list> # Manage rule packs
hooksmith eval # Evaluate rules (called by hooks.json, not directly)Rules are discovered from three tiers (highest precedence first):
- Project-level (
.hooksmith/hooks/): Applies to that project only - User-level (
~/.config/hooksmith/hooks/): Applies to all projects - Packs (
~/.config/hooksmith/packs/*/): Installed rule collections
When multiple rules share the same name, the highest-precedence one wins. A project rule with enabled: false and a matching name disables the lower-tier rule.
Every rule lives inside a rules: array:
rules:
- name: my-rule # Required — unique rule name
on: PreToolUse Bash # Event and optional tool matcher
match: command =~ pat # Mechanism: match, run, or prompt
deny: "Reason" # Action: deny, ask, or contexton: <Event> [ToolMatcher]
- Event (required):
PreToolUse,PostToolUse,Stop,UserPromptSubmit,SessionStart,SessionEnd,SubagentStart,SubagentStop,PostCompact,Notification, etc. - Tool matcher (optional): regex against
tool_name—Bash,Write|Edit, etc.
match — Pattern matching:
rules:
- name: block-rm
on: PreToolUse Bash
match: command =~ rm[[:space:]]+-rf[[:space:]]+(/|~|\$HOME)
deny: "Blocked: destructive rm targeting system or home directory."Available fields: command, file_path, content, user_prompt, tool_name, or any tool_input key.
run — Custom bash logic:
rules:
- name: sudo-guard
on: PreToolUse Bash
run: |
source "$HOOKLIB"
read_input
cmd=$(get_field command)
[[ "$cmd" =~ ^sudo ]] && echo "Root access not allowed"
deny: trueWhen deny: true, the script's stdout becomes the deny reason. No output = allow.
prompt — LLM-evaluated:
rules:
- name: security-review
on: PreToolUse Bash
prompt: "Review this bash command for security risks. Deny if it modifies system files, accesses credentials, or runs with elevated privileges."
ask: true| Action | Effect | Compatible events |
|---|---|---|
deny: "reason" |
Block the action | PreToolUse, PostToolUse, Stop, UserPromptSubmit, SubagentStop |
ask: "reason" |
Prompt user for approval | PreToolUse only |
context: "text" |
Inject context for Claude | All events |
| Field | Required | Default | Description |
|---|---|---|---|
name |
Yes | — | Unique rule name |
on |
Yes | — | Event and optional tool matcher |
match |
One of | — | Pattern: field =~ pattern (POSIX ERE) |
run |
One of | — | Script path or inline bash |
prompt |
One of | — | LLM prompt text |
deny |
One of | — | Block with reason (or true for run/prompt) |
ask |
One of | — | Ask for approval (PreToolUse only) |
context |
One of | — | Inject context message |
enabled |
No | true |
Set false to disable without removing |
Available in run scripts via source "$HOOKLIB":
read_input # Read stdin JSON into $INPUT
get_field <name> # Extract field (command, file_path, content, user_prompt, tool_name, cwd)
deny "reason" # Block with message
ask "reason" # Request user approval
context "text" # Inject context for Claude
log "message" # Debug to stderrbash tests/run-tests.shPure bash test runner, no dependencies. Covers all mechanisms and decision types.
jqbash3.2+- Claude Code v2.1.91+
See examples/ for working rules covering all three mechanisms.
Ask Claude to "create a hook rule" — the hooksmith skill provides guided rule creation.
- Hooks reference — Full event list, JSON payload shapes, decision fields
- Automate workflows with hooks — Hook patterns and use cases
- Plugins reference — Plugin structure,
bin/on PATH,hooks.jsonloading