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, and every task. For guardrails like blocking rm -rf or DROP TABLE, that universal reach is often the right call. Stricter controls are different. A rule that blocks any write outside one directory, or blocks all destructive git commands, creates constant friction in the sessions that do not need it.

Skills registered through the hooks frontmatter field solve this. They activate when you invoke the skill. Claude Code removes them automatically when the skill finishes or becomes inactive (hooks reference). The skill invocation is the signal: calling /careful states your intent ("I'm touching prod") and arms the constraints at the same time.

How skill-scoped hooks work

Skills can declare a hooks field in their YAML frontmatter. It uses the same configuration format as settings.json hooks (skills docs). Claude Code registers these hooks in memory for the current session.

The Claude Code documentation says 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: they stay active while the skill runs, rather than persisting across the whole session. So skills let you arm guardrails for the length of one task and no longer.

Skill hooks support all hook event types, including PreToolUse, PostToolUse, PermissionRequest, and Stop. They also support one field that settings.json and agent frontmatter do not honor: once. When once: true, the hook fires once per session and is then removed, which suits initialization checks (hooks reference).

The /hooks menu shows skill-registered hooks with a Session label, which sets them apart from project and user-level settings hooks (changelog v2.1.75).

When to use on-demand versus 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

Always-on hooks cost friction in every session that does not need them. On-demand hooks cost coverage: the guardrail is absent unless you invoke it, so you have to remember to call the skill.

Contrast: always-on versus on-demand

The always-on version applies to every session, whether you are touching prod or running a local demo:

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

The on-demand version uses a /careful skill. It arms the same hook only when you invoke 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