CtrlK
BlogDocsLog inGet started
Tessl Logo

hook-engineering

Design current Claude Code hooks with nested matcher-entry schema, five handler types, matcher/if boundaries, permissions-aware policy, portable scripts, MCP timing checks, and deterministic validation.

SKILL.md
Quality
Evals
Security

Hook Engineering

Capability

Design Claude Code hooks that run at lifecycle events without relying on the model to remember a rule. The current official configuration shape is nested: each event key maps to an array of matcher entries, and each matcher entry contains an inner hooks array of handler objects. Flat event arrays of handlers are legacy and must be migrated. [DOC]

Use hook-engineering to design or repair a hook configuration; then audit the resulting shape and local skill package with the toolkit gate ${CLAUDE_PLUGIN_ROOT}/scripts/check.sh at the plugin root. Reusable local assets are listed in assets/manifest.json; load assets/README.md for the asset inventory. [CONFIG]

Current Official Surface

Local reference: assets/hook-engineering-policy.json (the bundled policy) and knowledge/body-of-knowledge.md. The event list below is the single set declared in that policy — treat it as one tier, not a confirmed/candidate split. [CONFIG]

Event names (case-sensitive), the 22 in the vendored official spec and in scripts/validate_hooks.py (EVENTS): SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, PostToolUseFailure, Notification, SubagentStart, SubagentStop, Stop, StopFailure, TeammateIdle, TaskCompleted, InstructionsLoaded, ConfigChange, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, Elicitation, ElicitationResult, SessionEnd. A hooks.json naming any other event is rejected by the toolkit gate (HOOKS-CRITICAL unknown event name), so this list and the validator are one declaration. coverage_gap: documented surface, not runtime-probed; before relying on a less common event confirm it fires in the target session. [CÓDIGO]

Deterministic assets (2.16): do not hand-write the JSON. hook-scaffold emits and validates a hooks block (its scaffold_hook and validate_hook scripts); hook-rules turns a prose guideline into a validated hookify rule (its scaffold_rule and validate_rule scripts). This skill owns the design rationale; those two own the artifact. [CÓDIGO]

Handler types:

TypeRequired fieldsDeterministic use
commandcommandBest default for deterministic local policy and scripts.
httpurlDeterministic only if the endpoint is stable and controlled.
mcp_toolserver, toolCalls an already-connected MCP server; tolerate missing server/tool results.
promptpromptLLM judgment, not deterministic enforcement.
agentpromptExperimental agentic verifier; use cautiously and prefer command hooks for production policy.

Common handler fields are type, if, timeout, statusMessage, and once. [DOC]

Handler/event compatibility matters. Per the bundled policy (handler_event_compatibility), all five handler types are accepted on thirteen events: PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied, Stop, SubagentStop, TaskCompleted, TaskCreated, TeammateIdle, UserPromptSubmit, and UserPromptExpansion. A second group accepts command, http, and mcp_tool only, and SessionStart/Setup accept command and mcp_tool only. Outside the all-five group, prompt/agent handlers do not apply. [CONFIG]

coverage_gap: this compatibility map is documented schema from the bundled policy, not runtime-probed in a live session. The installed Claude Code version may expose fewer events or narrower handler support. Before relying on a less common event or on a prompt/agent handler, confirm it fires in the target session and record the residual gap. [SUPUESTO]

Source discrepancy [coverage_gap]: a second documented source (internal-plugin-quality-source / official-specs/ official-hook-spec.md) restricts prompt/agent to the 3 ToolUseContext events ONLY (PreToolUse, PostToolUse, PermissionRequest) — narrower than the bundled-policy list above. The gate ${CLAUDE_PLUGIN_ROOT}/scripts/validate_hooks.py enforces the conservative (3-event) rule, treating a prompt/ agent hook elsewhere as a "silent bomb". When they conflict, prefer the conservative rule and verify against the live runtime. Full matrix: references/hook-compatibility-matrix.md. [DOC]

When To Use

  • Guard high-impact tool use: protected paths, shell command classes, external domains, production deploys, or write operations in read-only workflows. [INFERENCIA]
  • Normalize or audit successful tool results after PostToolUse.
  • Block or inspect slash command expansion with UserPromptExpansion before the expanded prompt reaches Claude. coverage_gap: confirm the event fires in the target session before depending on it, else fall back to UserPromptSubmit. [SUPUESTO]
  • Route compliance or telemetry to a connected MCP server with mcp_tool, while handling connection timing as a non-blocking risk. [DOC]

Do not use hooks as a way to grant authority. Permission rules remain the baseline authority boundary. A PreToolUse hook can tighten restrictions with permissionDecision: "deny" even under permissive modes, but a hook allow cannot override deny rules from settings or managed policy. [DOC]

Design Rules

  1. Use the nested matcher-entry schema:

    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "if": "Bash(git push *)",
                "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-git-push.py",
                "args": []
              }
            ]
          }
        ]
      }
    }
  2. Keep matcher and if separate. matcher filters a matcher entry by event-specific fields such as tool name, command name, notification type, or MCP server name. if is a single permission-rule string on individual handlers and, per the bundled policy (tool_filtered_events_for_if), is evaluated only on the tool-filtered events PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied. On any other event a handler with if set never runs, so omit if there. Note this is a separate axis from handler availability: an event may still accept command, http, or mcp_tool handlers even where if does not evaluate — if support is narrower than handler support. [CONFIG]

  3. Use permissions for coarse allow/deny policy. Use hooks for deterministic checks, automation, narrower runtime denies, audit, and normalization. Never claim a hook allow bypasses permission policy. [DOC]

  4. Put slash-command expansion controls on UserPromptExpansion, not only UserPromptSubmit, when the risk is in the generated prompt. Match by command name and block unsafe expansions before Claude receives them. coverage_gap: confirm UserPromptExpansion fires in the target session, and keep a UserPromptSubmit fallback. [SUPUESTO]

  5. Make command hooks portable. Prefer ${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT}, or ${CLAUDE_PLUGIN_DATA} over absolute local paths; use exec form with args when quoting or path placeholders matter; verify scripts exist and are executable. [DOC]

  6. Treat mcp_tool handlers as integration hooks. The MCP server must already be connected; hooks do not start OAuth or connection flows. Avoid SessionStart and Setup unless the handler tolerates a not-connected result. [DOC]

  7. Prefer deterministic command or controlled http handlers for policy. Use prompt or agent only when judgment is needed; document that prompt/agent results depend on model behavior and that agent hooks are experimental. [DOC]

  8. Keep examples and evals concrete: include the exact event, matcher, handler type, policy boundary, validation command, and residual coverage_gap when runtime version or MCP availability is unknown. [CONFIG]

Correct Pattern

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-client-paths.py",
            "args": [],
            "timeout": 10,
            "statusMessage": "Checking protected client paths"
          }
        ]
      }
    ],
    "UserPromptExpansion": [
      {
        "matcher": "publish-client-deck",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command-expansion.py",
            "args": [],
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "audit-ledger",
            "tool": "record_hook_event",
            "input": {
              "event": "${hook_event_name}",
              "path": "${toolInput.file_path}"
            },
            "timeout": 15
          }
        ]
      }
    ]
  }
}

Interpolation tokens follow a camelCase convention (${toolInput.file_path}, not snake_case ${tool_input.file_path}). coverage_gap: the exact token set beyond toolInput and hook_event_name is not enumerated in the local reference; confirm field names against the installed version before depending on them. [SUPUESTO]

The command scripts read JSON from stdin and return structured JSON only when they must block, rewrite, or add context. A path guard that denies a protected write should return:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Protected client deliverable path requires explicit approval."
  }
}

Anti-Patterns

  • Flat schema: { "PreToolUse": [{ "type": "command", "command": "..." }] }.
  • if on Stop, SessionStart, or UserPromptSubmit; only PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied evaluate if, so the handler never runs elsewhere.
  • A hook that returns allow and claims it overrides project or managed deny rules.
  • Absolute local paths such as /Users/name/project/.claude/hooks/check.sh in committed hook configuration.
  • mcp_tool on SessionStart without a fallback for servers not connected yet.
  • Prompt or agent hooks used as the only control for irreversible policy decisions.

Validation Checklist

  • Event keys use the current case-sensitive official event names.
  • Every event value is an array of matcher entries with an inner hooks array.
  • Handler type is one of command, http, mcp_tool, prompt, or agent.
  • matcher and if are not treated as interchangeable.
  • if appears only on the tool-filtered events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied) and holds one permission rule.
  • prompt/agent handlers appear only on the events the bundled policy lists under all_five_types; never on command/mcp_tool-only events.
  • Permission boundaries are stated: hooks can tighten, not loosen, authority.
  • Less common events (e.g. UserPromptExpansion) carry a coverage_gap and a confirmed fallback when used for slash command expansion checks.
  • Command paths are portable, scripts are executable, and JSON parsing does not depend on unavailable local tools unless declared.
  • mcp_tool handlers document connected-server timing and non-blocking failure behavior.
  • Prompt/agent hooks are explicitly labeled judgment-based or experimental.
  • Output includes validation commands and any coverage_gap.

Related Skills

  • tool-permission-policy for baseline permission design.
  • mcp-engineering for MCP server and tool availability checks.

Related katas (toolkit)

  • katas-context-dilution-mitigation
  • katas-path-conditional-rules
  • katas-plan-mode-exploration
  • katas-pretooluse-guardrails
  • katas-posttooluse-normalization

Contract

  • Aceptación: hook con schema matcher-entry anidado, tipo de handler correcto, script portable con ${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT} o ${CLAUDE_PLUGIN_DATA}. [EXPLICIT]
  • Límites: diseña el hook; las reglas de permiso base las define tool-permission-policy. [EXPLICIT]
  • Casos borde: SessionStart no usa matcher como tool-events; formato plugin ≠ formato settings. [EXPLICIT]
  • Supuestos: matriz de eventos/handlers tomada de la política empaquetada (assets/hook-engineering-policy.json), no probada en runtime; coverage_gap: confirmar en la sesión destino que el evento usado dispara antes de depender de él [SUPUESTO]. [SUPUESTO]
  • Trade-off: automatización determinista vs latencia añadida por hook — mantener handlers rápidos. [EXPLICIT]

Packet

Capas del packet, cargables bajo demanda (disciplina ICM: una capa por vez, nunca todas juntas): references/ guías de profundidad (cargar UNA por etapa) · knowledge/ cuerpo de conocimiento · prompts/ prompts listos · examples/ salida de ejemplo · agents/ subagentes del packet · templates/ plantilla de output · assets/ recursos estáticos.

Repository
JaviMontano/claude-plugins
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.