2026-06-11 · skills

Chapter 5: Skills, hooks, and slash-commands

Compose your skill with parameters and tool permissions, then wire hooks and slash-commands into a repeatable review workflow.

Xiaoman · The Clockwork Hall

Craft alone is not enough. Today you fit Xiaoman with mechanisms that fire on their own.

Draft chapter. First cut to prove the format; it will be hardened before it is indexed.

What you’ll build

You will take the chapter 4 PR reviewer, which is a single passive skill, and turn it into a repeatable workflow that does three things. First, you parameterize the skill so a caller can scope it (review one file, or the whole diff against a base branch). Second, you limit which tools it may use, so the review can read but never write. Third, you bind it to a slash-command you fire by name and a hook that fires automatically at a defined moment. The result is one review procedure you can reach three ways: manually with /pr-reviewer, automatically by the model when relevant, and deterministically via a hook at a fixed point in your workflow.

Here is the key distinction to keep in mind: a skill is a procedure, a slash-command is one way to invoke it, and a hook is a deterministic trigger that does not depend on the model deciding anything. Skills are probabilistic, since the model chooses; hooks are guaranteed, since the harness runs them. You want both.

Prerequisites

  • Chapter 4 finished: a working pr-reviewer skill the agent already discovers.
  • Your agent supports skill frontmatter fields, slash-commands, and hooks. The field names below are just examples that follow the documented shape, and they change across versions, so check the official docs for the current ones.
  • A repo where you can safely test an automatic trigger without spamming a real PR.
  • For the hook step, jq available to parse the hook’s JSON input.

Steps

1. Parameterize the skill with arguments

Edit SKILL.md so the instructions accept an explicit scope from the caller instead of always reviewing everything. Skills support argument substitution: $ARGUMENTS expands to everything typed after the command, and $1, $2 (or named arguments) pick out positions. Declare an argument-hint so autocomplete shows callers what to pass.

---
name: pr-reviewer
description: >-
  Reviews a pull request or diff for correctness, tests, security, and clarity.
  Use when the user asks to review a PR, review changes, or check a diff.
argument-hint: "[path-or-base-branch]"
allowed-tools: Read, Grep, Bash(git diff *), Bash(gh pr diff *)
---

# PR Reviewer

Determine the scope from the argument:
- If $1 looks like a path, review only that file's changes.
- If $1 looks like a branch (e.g. main), review the diff against it.
- If $ARGUMENTS is empty, review the full diff of the current PR.

## Diff under review
!`git diff ${1:-HEAD}`

(then run the four-dimension checklist from Chapter 4 over the diff above)

2. Bound tool permissions to least privilege

A review only reads; it must never write, push, or merge. In the allowed-tools frontmatter field, list only the tools the skill may use without prompting. Here is the easy thing to miss: allowed-tools pre-approves the tools you list so the agent does not stop to ask, but on its own it does not remove the other tools. To actually block dangerous operations, pair it with deny rules in your permission settings, or list them in disallowed-tools.

# In SKILL.md frontmatter: pre-approve only read-shaped tools
allowed-tools: Read, Grep, Bash(git diff *), Bash(gh pr diff *)
// In .claude/settings.json: hard-deny write/merge across the board (illustrative)
{
  "permissions": {
    "deny": [
      "Bash(git push *)",
      "Bash(git commit *)",
      "Bash(gh pr merge *)"
    ]
  }
}

Tightening permissions here is not red tape. A reviewer that simply cannot push is one you can let run automatically without watching it.

3. Add a slash-command

Because the folder name is the command, your skill at .claude/skills/pr-reviewer/SKILL.md is already invocable as /pr-reviewer. A teammate runs the exact same review with one phrase, passing scope as arguments. If you want the command to be manual-only (never auto-triggered by the model), add disable-model-invocation: true.

/pr-reviewer                 # review the whole current diff
/pr-reviewer src/auth.py     # review just one file's changes
/pr-reviewer main            # review the diff against main

4. Wire a hook for deterministic, automatic review

A hook runs at a defined lifecycle moment regardless of what the model decides. Configure it in settings.json under the event you care about. A common shape: on PreToolUse for the git-commit tool, run a script that triggers the review before the commit lands. Hooks communicate back through exit codes (exit 0 allows, exit 2 blocks) or structured JSON on stdout.

// .claude/settings.json (illustrative; confirm event/field names in the docs)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git commit *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/review-before-commit.sh"
          }
        ]
      }
    ]
  }
}
#!/bin/bash
# review-before-commit.sh: block the commit if review finds blockers (illustrative)
DIFF=$(git diff --cached)
if [ -z "$DIFF" ]; then exit 0; fi   # nothing staged, nothing to review

# run your reviewer over the staged diff; suppose it writes a verdict file
# if a blocker is found, deny the commit and tell the model why
if grep -q "REQUEST CHANGES" review-verdict.txt; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Reviewer found blockers. Fix them before committing."
    }
  }'
else
  exit 0
fi

If you run the agent in-process with the SDK, you can attach the same guardrail in code instead of through an external script. In Python you register a callback on the PreToolUse event with HookMatcher and return permissionDecision: "deny" to block; in TypeScript you attach a function under hooks.PreToolUse and return { decision: "block", continue: false }.

import os
import anyio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher

async def review_before_commit(input_data, tool_use_id, context):
    if input_data["tool_name"] != "Bash":
        return {}
    command = input_data["tool_input"].get("command", "")
    if "git commit" not in command:
        return {}   # fire only on git commit, not on all of Bash
    verdict = open("review-verdict.txt").read() if os.path.exists("review-verdict.txt") else ""
    if "REQUEST CHANGES" in verdict:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": "Reviewer found blockers. Fix them before committing.",
            }
        }
    return {}   # clean, let the commit through

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Grep", "Bash"],
    hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[review_before_commit])]},
)

async def main():
    async with ClaudeSDKClient(options=options) as client:
        await client.query("Fix the off-by-one in refund and commit it.")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)
import { query } from "@anthropic-ai/claude-agent-sdk";
import type { HookJSONOutput } from "@anthropic-ai/claude-agent-sdk";
import { existsSync, readFileSync } from "fs";

const q = query({
  prompt: "Fix the off-by-one in refund and commit it.",
  options: {
    allowedTools: ["Read", "Grep", "Bash"],
    hooks: {
      PreToolUse: [
        {
          matcher: "Bash",
          hooks: [
            async (input: any): Promise<HookJSONOutput> => {
              const command = input.tool_input?.command ?? "";
              if (input.tool_name !== "Bash" || !command.includes("git commit")) {
                return { continue: true }; // fire only on git commit
              }
              const verdict = existsSync("review-verdict.txt")
                ? readFileSync("review-verdict.txt", "utf8")
                : "";
              if (verdict.includes("REQUEST CHANGES")) {
                return {
                  decision: "block",
                  stopReason: "Reviewer found blockers. Fix them before committing.",
                  continue: false,
                };
              }
              return { continue: true }; // clean, let the commit through
            },
          ],
        },
      ],
    },
  },
});

for await (const message of q) {
  console.log(message);
}

5. Make it repeatable and confirm all paths reach the same skill

The point of putting these together is to have one source of truth. The slash-command, the model’s automatic invocation, and the hook should all drive the same SKILL.md procedure, so the review comes out the same no matter how it started. Run a real change through each path and compare what the output looks like.

Path A: type /pr-reviewer main         -> same checklist, scoped to vs-main diff
Path B: ask "review my changes"        -> model loads the skill, same checklist
Path C: attempt git commit             -> hook fires, same checklist, can block

Learned: firing without being askedEven when you never ask for a review, a hook calls Xiaoman the moment you git commit and runs the same review, blocking the commit if it finds a blocker, so you do not have to remember to invoke it.

How to verify

  • Invoke /pr-reviewer src/auth.py and confirm the review covers only that file’s changes, proving the argument scoped it.
  • Watch the tool calls during a review and confirm only read-shaped tools fire. Attempt a write and confirm the deny rule stops it.
  • Stage a change with a deliberate blocker and try to commit. Confirm the hook fires unprompted and blocks the commit with the reason text.
  • Stage a clean change and commit. Confirm the hook lets it through (exit 0), proving the trigger is scoped and not just blanket-blocking.

Learned: confirming it knows when to hold backYou can stage a change with a blocker and watch the hook stop the commit, then stage a clean change and watch it pass, confirming the reflex fires only when it should and does not block every commit.

Recap

Your reviewer now snaps together from parts. Arguments scope it, allowed-tools plus deny rules set its bounds, the folder name gives you a slash-command, and a PreToolUse hook runs it deterministically. Keep this mental model: the skill is the single procedure; the slash-command and model-invocation are two probabilistic ways into it; the hook is the guaranteed one, wired to a lifecycle event. Keeping these separate is what makes it a workflow instead of a one-shot prompt. The next chapter steps up to subagents and orchestration, and lays out when multi-agent setups help you and when they hurt.

Common pitfalls

  • Permissions too broad. Pre-approving every tool, or forgetting that allowed-tools does not remove tools, lets a “reviewer” push or merge. Pair least-privilege allowed-tools with explicit deny rules.
  • Hook fires too often. A hook with a loose matcher runs a review on every tool call and drives you crazy. Use the if condition to scope it to the exact tool and command (Bash(git commit *)), not all of Bash.
  • Parameters hidden or never passed. If the skill assumes a scope the caller never sets, the results drift. Spell out the scope with $ARGUMENTS / $1 and document it with argument-hint.
  • Blocking the wrong way. A hook that exits non-zero with an unhelpful message just confuses the agent. Use exit 2 or a structured deny decision with a clear permissionDecisionReason so the model knows what to fix.

With a hook in place, Xiaoman does something on its own before you say a word, catching a problem at pre-commit, like a pet that learns to fetch your slippers the moment you walk in. Delightful, and a little out of hand. The Clockwork Hall lights up.

Just lit The Clockwork Hall · 6 / 16 lit

The work piles up past what one of it can handle; it glances at you and stops short of asking. Next: the Hall of Doubles.

Sources

  1. Anthropic Agent Skills documentation · official
  2. Claude Code hooks reference · official
UP NEXT · CHAPTER 6 Subagents & orchestration (and when not to)