Skip to content

On-Demand Skill Hooks: Session-Scoped Guardrails via Skill Invocation

Register PreToolUse hooks through a skill invocation to arm strict guardrails for a single session — without imposing that friction on every workflow.

The Problem with Always-On Hooks

Always-on hooks in .claude/settings.json apply to every session, every developer, every task. For guardrails like blocking rm -rf or DROP TABLE, that universality is often appropriate. For stricter controls — blocking any write outside a specific directory, blocking all destructive git commands — always-on enforcement creates constant friction in the sessions that don't need it.

Skills registered through the hooks frontmatter field solve this. They activate when the skill is invoked and are automatically removed when the skill finishes or becomes inactive (hooks reference). The skill invocation is the signal: calling /careful communicates intent ("I'm touching prod") and arms the constraints simultaneously.

How Skill-Scoped Hooks Work

Skills can declare a hooks field in their YAML frontmatter using the same configuration format as settings.json hooks (skills docs). These hooks are registered in memory for the current session.

Per the Claude Code documentation, skill hooks "use the same configuration format as settings-based hooks but are scoped to the component's lifetime and cleaned up when it finishes." The hooks are component-scoped — active while the skill is running — rather than persistent across the whole session. This makes skills an effective way to temporarily arm guardrails for the duration of a specific task.

Skill hooks support all hook event types — including PreToolUse, PostToolUse, PermissionRequest, and Stop — plus one additional field not honored in settings.json or agent frontmatter: once. When once: true, the hook fires once per session and is then removed — useful for initialization checks (hooks reference).

Hook source is shown in the /hooks menu with a Session label, distinguishing skill-registered hooks from project or user-level settings hooks (changelog v2.1.75).

When to Use On-Demand vs. Always-On

Scenario On-demand (skill hook) Always-on (settings.json)
Guardrail correct in one context, friction elsewhere Yes No
Rule applies to all developers on all tasks No Yes
Touching production systems Yes
Working in a restricted directory Yes
Debugging a fragile or high-stakes system Yes
Team-wide package manager enforcement No Yes

The cost of always-on hooks is friction in every session that doesn't need them. The cost of on-demand hooks is that the guardrail is absent unless explicitly invoked — the engineer must remember to call the skill.

Contrast: Always-On vs. On-Demand

Always-on — applies to every session, whether touching prod or running a local demo:

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": ".claude/hooks/block-destructive.sh"}]
    }]
  }
}

On-demand via /careful skill — arms the same hook only when the engineer invokes the skill:

# .claude/skills/careful/SKILL.md
---
name: careful
description: Arms strict guardrails for this session. Invoke when touching
  production systems, running migrations, or operating in restricted
  directories. Blocks rm -rf, DROP TABLE, force-push, and kubectl delete.
hooks:
  PreToolUse:
    - matcher: Bash
      hooks:
        - type: command
          command: .claude/hooks/block-destructive.sh
---

You are operating in careful mode. Every destructive command will be blocked.
Confirm with the user before proceeding with any irreversible operation.

Example

A /careful skill registers a PreToolUse hook that blocks rm -rf, DROP TABLE, git push --force, and kubectl delete. The hook script reads the Bash command from stdin and denies any match:

#!/bin/bash
# .claude/hooks/block-destructive.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE 'rm -rf|DROP TABLE|git push --force|git push -f|kubectl delete'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked in careful mode — confirm intent before proceeding"
    }
  }'
else
  exit 0
fi

A /freeze variant uses the same mechanism but blocks any Edit or Write call outside a specific directory:

#!/bin/bash
# .claude/hooks/freeze-writes.sh
TOOL=$(jq -r '.tool_name')
FILE=$(jq -r '.tool_input.path // .tool_input.file_path // empty')
ALLOWED_PREFIX="/home/user/project/src"

if [[ "$TOOL" == "Edit" || "$TOOL" == "Write" ]]; then
  if [[ -n "$FILE" && "$FILE" != "$ALLOWED_PREFIX"* ]]; then
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Writes outside /src are blocked in freeze mode"
      }
    }'
    exit 0
  fi
fi
exit 0

The skill frontmatter wires it in:

hooks:
  PreToolUse:
    - matcher: "Edit|Write"
      hooks:
        - type: command
          command: .claude/hooks/freeze-writes.sh

When the skill finishes, the hook is removed. No cleanup required.

Key Takeaways

  • Skill-defined hooks are component-scoped: they activate when the skill runs and are removed when it finishes (hooks reference)
  • Skill invocation is both the human signal ("I need prod-safe guardrails") and the system action (arming those guardrails)
  • The once field, honored only in skill hooks (ignored in settings files and agent frontmatter), fires a hook once per session then removes it — useful for initialization guardrails
  • Session-sourced hooks appear with a Session label in the /hooks menu, distinct from project and user settings hooks
  • The tradeoff: on-demand hooks require the engineer to invoke the skill; always-on hooks enforce without relying on that discipline
  • Use on-demand hooks for context-specific restrictions; use always-on hooks for universal team standards
Feedback