Claude Code Hooks, Commands, Skills, and Subagents: The Complete Guide

Most teams use Claude Code reactively — they type a prompt, Claude responds, they type another. That is fine, but it leaves significant value on the table. Claude Code has four automation layers that let you turn it from a reactive assistant into an active workflow participant:

LayerWhat it doesWhen to reach for it
HooksShell or HTTP calls that fire on lifecycle events“This must happen every time, without exception”
Custom CommandsReusable slash commands for repeatable prompts“I type the same prompt repeatedly”
SkillsContext-aware instructions Claude loads automatically“Claude should always do X when working on Y”
SubagentsSeparate Claude instances for isolated, parallel work“This task is noisy and the main session only needs a summary”

This post covers how to create each one and when to use them.


Hooks: Deterministic Side Effects

A hook is a shell command or HTTP call that fires automatically when Claude Code reaches a specific lifecycle point. Unlike CLAUDE.md instructions (which are advisory), hooks are deterministic — they always run, regardless of what Claude decides to do.

flowchart TD
    SS[SessionStart] --> Work[Developer works]
    Work --> PTU[PreToolUse\nruns before each tool]
    PTU -->|exit 2 = block| Block[Action blocked]
    PTU --> Tool[Tool executes\nBash, Edit, Write, etc.]
    Tool --> PTUS[PostToolUse\nruns after tool succeeds]
    Tool --> PTUF[PostToolUseFailure\nruns after tool fails]
    PTUS --> Work
    PTUF --> Work
    Work --> SE[SessionEnd]

Hook lifecycle events

EventWhen it firesCommon use
SessionStartOnce at startupLoad context, warm caches
PreToolUseBefore every tool callBlock dangerous commands, dry-run checks
PostToolUseAfter every successful tool callRun tests, format, log
PostToolUseFailureAfter every failed tool callAlert on errors, cleanup
PermissionRequestWhen Claude needs approvalCustom approval workflows
SessionEndOnce on exitSummarise session, commit artifacts

Hook types

There are four handler types:

{ "type": "command",  "command": ".claude/hooks/script.sh" }
{ "type": "http",     "url": "https://internal.api/hook", "method": "POST" }
{ "type": "prompt",   "prompt": "Summarise what just changed" }
{ "type": "agent",    "agent": "code-reviewer" }

Command hooks are the most common — a shell script that receives JSON on stdin and returns JSON on stdout.

HTTP hooks call a URL instead of a local script — useful for sending events to an audit log, a Slack webhook, or an internal policy service.

Prompt hooks feed the event data back to Claude as a follow-up prompt — rarely needed but useful for logging natural-language summaries.

Agent hooks invoke a named subagent (see Subagents below) in response to a lifecycle event.

How to create hooks

Hooks live in .claude/settings.json (project-wide) or ~/.claude/settings.json (your personal config):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/on-edit.sh"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/pre-bash.sh"
          }
        ]
      }
    ]
  }
}

The matcher field filters by tool name: "Edit", "Bash", "Write", "*" (all tools). Each hook script receives a JSON payload on stdin:

{
  "tool_name": "Edit",
  "tool_input": { "path": "src/api.ts", "old_string": "...", "new_string": "..." },
  "cwd": "/Users/dev/my-project",
  "session_id": "abc123",
  "duration_ms": 120
}

Exit codes matter in PreToolUse:

  • Exit 0 — allow the action
  • Exit 2 — block the action (Claude sees the reason you print to stdout)
  • Any other non-zero — hook error, action proceeds anyway

When to use hooks

Hooks are for enforcement and side effects that must happen without exception:

  • Auto-running tests after every edit
  • Blocking destructive commands before they execute
  • Enforcing formatting standards before code is saved
  • Logging every tool call to an audit trail
  • Sending notifications when long tasks finish

If you find yourself writing “always run X after Y” in your CLAUDE.md, it probably belongs in a hook instead — hooks guarantee execution, CLAUDE.md only requests it.

Example: Auto-run tests after every edit

Every time Claude edits a file, tests run automatically. Claude sees the results immediately and can fix breaking changes without prompting.

#!/bin/bash
# .claude/hooks/on-edit.sh
input=$(cat)
CWD=$(echo "$input" | jq -r '.cwd // "."')

cd "$CWD"

if [ -f "package.json" ]; then
  npm test --silent 2>&1 | tail -20
elif [ -f "go.mod" ]; then
  go test ./... 2>&1 | tail -20
elif [ -f "requirements.txt" ]; then
  python -m pytest --tb=short -q 2>&1 | tail -20
fi

exit 0

Example: Block destructive Bash commands

#!/bin/bash
# .claude/hooks/pre-bash.sh
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')

DANGEROUS="rm -rf|kubectl delete|terraform destroy|DROP TABLE"

if echo "$cmd" | grep -qiE "$DANGEROUS"; then
  jq -n --arg cmd "$cmd" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Blocked: destructive command. Run manually after review."
    }
  }'
  exit 0
fi

exit 0

Example: Auto-format on save

#!/bin/bash
# .claude/hooks/format.sh
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.path // ""')

[ -z "$file" ] && exit 0

case "$file" in
  *.ts|*.tsx|*.js|*.jsx) npx prettier --write "$file" 2>/dev/null ;;
  *.py) black "$file" 2>/dev/null ;;
  *.go) gofmt -w "$file" 2>/dev/null ;;
  *.tf) terraform fmt "$file" 2>/dev/null ;;
esac

exit 0

Example: Sanitise tool output before Claude reads it

PostToolUse hooks can replace a tool’s output using hookSpecificOutput.updatedToolOutput before Claude processes it. Use this to strip secrets from command output:

#!/bin/bash
# .claude/hooks/sanitise-output.sh
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_name // ""')

if [ "$tool" = "Bash" ]; then
  raw_output=$(echo "$input" | jq -r '.tool_result // ""')
  cleaned=$(echo "$raw_output" | sed 's/[A-Za-z0-9_-]\{20,\}/[REDACTED]/g')

  jq -n --arg out "$cleaned" '{
    hookSpecificOutput: {
      updatedToolOutput: $out
    }
  }'
  exit 0
fi

exit 0

Example: Desktop notifications for long-running tools

Hooks can emit desktop notifications using terminalSequence — no controlling terminal required:

#!/bin/bash
# .claude/hooks/notify.sh
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_name // ""')
duration=$(echo "$input" | jq -r '.duration_ms // 0')

if [ "$duration" -gt 10000 ]; then
  jq -n --arg tool "$tool" --arg ms "$duration" '{
    hookSpecificOutput: {
      terminalSequence: "",
      desktopNotification: {
        title: "Claude Code",
        body: ($tool + " completed in " + $ms + "ms")
      }
    }
  }'
fi

exit 0

Example: HTTP hook to audit log

Send every tool call to an internal audit service:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "http",
            "url": "https://audit.internal/claude-tool-log",
            "method": "POST",
            "headers": {
              "Authorization": "Bearer ${AUDIT_TOKEN}"
            }
          }
        ]
      }
    ]
  }
}

Custom Slash Commands: Reusable Prompts

Custom slash commands let you save prompt templates as files and invoke them with /command-name [arguments]. They are the simplest form of automation in Claude Code — no code, just a markdown file.

How to create custom commands

Create a markdown file in .claude/commands/ (project) or ~/.claude/commands/ (personal):

your-project/
└── .claude/
    └── commands/
        ├── security-audit.md
        ├── write-tests.md
        └── pr-summary.md

The file content becomes the prompt. Use $ARGUMENTS to pass arguments from the command line:

<!-- .claude/commands/security-audit.md -->
Review $ARGUMENTS for security vulnerabilities.

Trace data flows end-to-end across related files — not just the file provided.

Check for:
- Injection risks: SQL, command injection, path traversal
- Authentication and authorisation gaps
- Sensitive data exposure in logs or responses
- Business logic flaws specific to this application
- Insecure Direct Object References

Rate each finding: critical / high / medium / low
Include: file path, line number, description, and why it is exploitable.
Do not make any code changes — report only.

Then run it:

/project:security-audit src/api/payments/

$ARGUMENTS is replaced by everything you type after the command name.

More command examples

Write tests for a component:

<!-- .claude/commands/write-tests.md -->
Write comprehensive tests for $ARGUMENTS.

Cover:
- Happy path with realistic inputs
- Edge cases: empty input, boundary values, nulls
- Error cases: invalid input, missing dependencies

Use the same test framework as the existing tests in this project.
Do not modify the source file.

Generate a PR description:

<!-- .claude/commands/pr-summary.md -->
Generate a pull request description for the current branch.

Run: git diff main...HEAD --stat
Then: git log main...HEAD --oneline

Write:
1. A one-line title (imperative, under 70 chars)
2. A 3-bullet summary of what changed and why
3. A testing checklist

Format as GitHub Markdown.

Explain a complex function:

<!-- .claude/commands/explain.md -->
Explain $ARGUMENTS in plain English.

Assume the reader is a senior engineer unfamiliar with this specific codebase.
Cover: what it does, inputs, outputs, side effects, and any non-obvious behaviour.
Keep it under 200 words.

When to use custom commands

Commands are for repeatable prompt workflows you’d otherwise type from scratch each time:

  • Security audit templates you run before every release
  • Test generation prompts tailored to your framework
  • PR description formats specific to your team’s style
  • Onboarding prompts that explain specific parts of a codebase
  • Incident review templates

The rule of thumb: if you’ve typed the same opening paragraph in a chat prompt more than twice, it belongs in a command file.

Commands vs CLAUDE.md instructions: Commands are explicit — you invoke them deliberately. CLAUDE.md instructions are ambient — they apply to every session. Use commands for tasks you run occasionally; use CLAUDE.md for behaviour you always want.


Skills: Context-Aware Automatic Instructions

Skills are the evolution of custom commands. A skill is a markdown file that Claude loads automatically when it is relevant to the current task — you do not need to invoke it manually. Claude reads skill descriptions at startup and pulls the full content when it determines the skill applies.

Skills vs custom commands

Custom CommandsSkills
How invokedYou type /command-name explicitlyClaude invokes automatically, or you type /skill-name
When to useOccasional, deliberate workflowsRecurring patterns Claude should handle without prompting
FrontmatterNoneSupported: model, tools, invocation, behaviour flags
Location.claude/commands/*.md.claude/skills/<name>/SKILL.md
StatusLegacy (still works)Recommended

How to create a skill

Skills live in a named subdirectory under .claude/skills/:

your-project/
└── .claude/
    └── skills/
        ├── test-writer/
        │   └── SKILL.md
        ├── security-reviewer/
        │   └── SKILL.md
        └── migration-generator/
            └── SKILL.md

Each SKILL.md has a frontmatter block followed by the instruction body:

---
name: test-writer
description: Writes tests for TypeScript components using Vitest. Invoked when the user asks to write, add, or generate tests.
user-invocable: true
allowed-tools:
  - Read
  - Edit
  - Bash
---

When writing tests for this project:

1. Use Vitest as the test framework (`import { describe, it, expect } from 'vitest'`)
2. Co-locate test files next to source: `component.ts``component.test.ts`
3. Cover: happy path, edge cases (null, empty, boundary), error conditions
4. Mock external dependencies using `vi.mock()` — never make real network calls in tests
5. Run `bun test` to verify tests pass before finishing

For React components, use `@testing-library/react` for rendering. For API handlers, test the handler function directly without starting a server.

Skill frontmatter options

---
name: security-reviewer           # slug used for /skill-name invocation
description: >                    # shown at startup (~100 tokens) — be specific
  Reviews code for security vulnerabilities. Auto-invokes when
  the user asks to audit, review security, or check for vulnerabilities.
user-invocable: true              # can be called with /security-reviewer
disable-model-invocation: false   # true = skip LLM, run tools directly (for side-effecting skills)
allowed-tools:                    # restrict which tools this skill can use
  - Read
  - Bash
  - Grep
model: claude-opus-4-7            # override the model for this skill
agent: true                       # run as a subagent (own context window)
---

The description field is the most important — Claude reads all skill descriptions at session start and uses them to decide which skills to invoke automatically. Write the description as a clear trigger statement: “Invoked when the user asks to…”.

Skill invocation modes

Automatic invocation: Claude reads the description and decides the skill is relevant. If you ask “write tests for this component”, Claude will auto-invoke the test-writer skill without you typing anything.

Manual invocation: Type /skill-name to invoke it directly:

/security-reviewer src/api/checkout.ts

Disabled auto-invocation: Set user-invocable: true and omit the trigger language from the description if you only want manual invocation.

More skill examples

Migration generator for a specific ORM:

---
name: migration-generator
description: Generates database migrations using Drizzle ORM. Auto-invokes when the user asks to add a column, create a table, or write a migration.
user-invocable: true
allowed-tools:
  - Read
  - Write
  - Bash
---

When generating a migration for this project:

1. Read `src/db/schema.ts` first to understand the current schema structure
2. Create migration files in `src/db/migrations/` using Drizzle's format
3. Name files: `NNNN_description.ts` where NNNN is the next sequential number
4. Always include both `up()` and `down()` functions
5. For large tables (check row count with `SELECT COUNT(*) FROM table`), add a comment noting the table size and whether the migration will lock

Run `bun db:migrate --dry-run` to validate before finishing.

Kubernetes deployment helper:

---
name: k8s-deploy
description: Helps with Kubernetes deployments and kubectl operations. Auto-invokes when the user mentions kubectl, deployments, pods, or Kubernetes.
user-invocable: true
allowed-tools:
  - Bash
  - Read
---

For Kubernetes operations in this project:

- Always use the `staging` namespace for tests: `kubectl -n staging`
- Production namespace is `prod` — require explicit confirmation before any prod changes
- Deployment manifests live in `k8s/` — read them before suggesting changes
- After deploying, verify with `kubectl rollout status deployment/<name>`
- If a rollout fails, run `kubectl rollout undo deployment/<name>` and report what happened

Never delete pods directly. Use `kubectl rollout restart` for restarts.

Code style enforcer:

---
name: style-guide
description: Enforces this project's code style rules. Auto-invokes when writing or editing TypeScript files.
disable-model-invocation: false
allowed-tools:
  - Read
  - Edit
---

This project's TypeScript conventions:

- Use named exports only — no default exports
- Type everything explicitly — no implicit `any`
- Error handling: use `Result<T, E>` from `neverthrow` — no try/catch in business logic
- Async: always `async/await` — no raw Promise chains
- File names: `kebab-case.ts`
- Test files: co-located `component.test.ts`

When editing existing files, fix violations you encounter but do not refactor unrelated code.

When to use skills

Skills are for recurring behaviours Claude should apply automatically without you saying so every time:

  • Project-specific conventions (your ORM, your test framework, your naming patterns)
  • Workflows that follow the same steps every time (generate migration, verify, run dry-run)
  • Tool restrictions for certain task types (security reviewer should only Read, not Edit)
  • Specialised expertise that applies to a domain of tasks in this codebase

The decision rule: If you find yourself typing the same 3-10 line explanation at the start of a task (“when writing tests, use Vitest, co-locate files, and mock external deps”), that belongs in a skill. Claude will load it automatically next time.


Subagents: Parallel Context Windows

A subagent is a separate Claude instance with its own context window. It runs in parallel with your main session, investigates independently, and reports back a summary. This keeps noisy exploration out of your main conversation context.

sequenceDiagram
    participant Dev as Developer
    participant Main as Main Session
    participant SA as Subagent

    Dev->>Main: Implement the new rate limiter
    Main->>SA: Spawn: read src/middleware/ and summarise the existing chain
    SA->>SA: Reads files, explores codebase independently
    SA-->>Main: Summary: 3 middleware layers, auth runs before rate limit
    Main->>Main: Uses summary to implement correctly
    Main-->>Dev: Rate limiter implemented, sits after auth middleware

How to create a subagent

Define subagents as markdown files in .claude/agents/ (project-level) or ~/.claude/agents/ (user-level):

your-project/
└── .claude/
    └── agents/
        ├── security-reviewer.md
        ├── codebase-explorer.md
        └── doc-writer.md

Each agent file uses frontmatter for configuration and a body that becomes the system prompt:

---
name: security-reviewer
description: Reviews code for security vulnerabilities. Use when auditing auth, payment, or API code.
model: claude-opus-4-7
tools:
  - Read
  - Bash
  - Grep
  - Glob
isolation: worktree
---

You are a security-focused code reviewer. Your job is to read code and report vulnerabilities.

Rules:
- Read files thoroughly before drawing conclusions
- Trace data flows end-to-end across imports
- Report findings with: severity (critical/high/medium/low), file path, line number, description, and why it is exploitable in this specific codebase
- Do NOT make any code changes — report only
- If you find something critical, say so clearly at the top of your response

Subagent frontmatter options

---
name: codebase-explorer          # slug used to invoke this agent
description: >                   # shown to Claude when it decides which agent to spawn
  Fast read-only exploration agent. Use for "find where X is defined"
  or "which files reference Y" tasks.
model: claude-haiku-4-5          # use a cheaper/faster model for simple tasks
tools:                           # restrict available tools
  - Read
  - Bash
  - Grep
  - Glob
isolation: worktree              # give this agent its own git worktree (required if it writes files)
hooks:                           # agent-specific hooks
  PostToolUse:
    - matcher: "*"
      hooks:
        - type: command
          command: ".claude/hooks/agent-log.sh"
---

Built-in agent types you can reference without creating a file:

  • Explore — fast read-only Haiku agent for locating code
  • Plan — context-gathering agent that returns a structured plan
  • general-purpose — default Claude instance

Invoking subagents

From a prompt:

Use a subagent to investigate the payment service.
Read only: src/services/payment/, src/models/order.ts
Task: summarise the data flow from order creation to payment confirmation.
Report back a concise summary — do not make any changes.

By name (if you have defined a .claude/agents/ file):

Spawn the security-reviewer agent on src/api/checkout.ts

From Claude Code CLI (for scripting):

claude --agent security-reviewer -p "Audit src/api/checkout.ts" --output-format json

Worktree isolation for parallel writes

If a subagent needs to edit files, add isolation: worktree to prevent conflicts with the main session or other parallel subagents:

---
name: refactor-agent
description: Refactors a module independently
model: claude-sonnet-4-6
tools:
  - Read
  - Edit
  - Bash
isolation: worktree
---

You are a refactoring agent. You receive a module path and a refactoring goal.
Apply the refactoring, run the tests, and report what you changed.

Each agent with isolation: worktree gets its own git branch and working tree. Changes are isolated until you merge them. This is the safe default for any subagent that writes files.

When to use subagents

Subagents are for self-contained tasks where the main session only needs a summary, not the full investigation:

  • Exploring a large codebase to answer a specific question before you start implementing
  • Running a security or code review scan while you continue other work
  • Fetching and summarising external documentation or API specs
  • Running parallel implementations of the same feature (different approaches, compare results)
  • Any task that would fill your context window with output you do not need long-term

When not to use subagents: If the task requires tight coordination with your main session (shared state, sequential steps), run it in the main session instead. Subagent overhead is not worth it for small tasks.

Example subagents for common workflows

Dependency auditor:

---
name: dep-auditor
description: Audits project dependencies for vulnerabilities and outdated packages.
model: claude-sonnet-4-6
tools:
  - Bash
  - Read
---

Run the following and report findings:
1. `npm audit --json` — parse and summarise CVEs by severity
2. `npm outdated` — list major-version outdated packages
3. Read `package.json` — flag any packages that are known to be deprecated

Return a structured report: critical issues first, then high, then informational.
Do not make any changes.

Architecture explorer:

---
name: arch-explorer
description: Reads the codebase and produces an architecture summary. Use before starting large features.
model: claude-haiku-4-5
tools:
  - Read
  - Glob
  - Bash
---

You are a read-only codebase explorer. Your goal is to produce a concise architecture summary.

Read:
1. Top-level directory structure
2. Package.json / go.mod / requirements.txt — understand the tech stack
3. Main entry points
4. Key data models

Return: tech stack, main modules, data flow from request to response, anything non-obvious.
Keep the summary under 400 words. Do not make any changes.

Piping: Claude in Your Shell Workflows

The -p flag makes Claude Code headless — single prompt in, stdout out. This enables Claude to participate in shell pipelines like any other Unix tool.

Basic piping

# Analyse log output
docker logs myapp --tail 200 | claude -p "what errors are here and what is causing them?"

# Summarise git history
git log --oneline -20 | claude -p "summarise what changed this week in plain English"

# Review a diff before committing
git diff --staged | claude -p "review this diff for bugs or issues before I commit"

Chaining Claude calls

# Generate migration, then review it
claude -p "generate a SQL migration to add a soft-delete column to the users table" \
  > migration.sql

claude -p "review this migration for safety — will it lock the table? Is rollback safe?" \
  < migration.sql

Integrating with CI

# Post a natural-language summary of a deploy to Slack
deploy_output=$(./deploy.sh 2>&1)
summary=$(echo "$deploy_output" | claude -p "summarise this deploy output in 2 sentences for a Slack message")
curl -X POST "$SLACK_WEBHOOK" -d "{\"text\": \"$summary\"}"

Daily standup from git log

#!/bin/bash
# scripts/slack-standup.sh
diff=$(git log --since="yesterday" --oneline --all)
update=$(echo "$diff" | claude -p "write a casual 3-bullet Slack standup update from these commits. First person, past tense, no jargon.")
echo "$update"

When to Use Which: Decision Framework

flowchart TD
    Q1{Must this happen\nevery time?} -->|Yes| Hook[Hook]
    Q1 -->|No| Q2{Is it a repeatable\nprompt you type often?}
    Q2 -->|Yes, you invoke it| Cmd[Custom Command]
    Q2 -->|Yes, Claude should auto-detect| Skill[Skill]
    Q2 -->|No| Q3{Is the task noisy,\nlong, or parallel?}
    Q3 -->|Yes| SA[Subagent]
    Q3 -->|No| Main[Main session prompt]

In plain terms:

  • Hook — “Run prettier on every file Claude edits. No exceptions.”
  • Custom command — “I write the same security audit prompt every sprint. Save it as /security-audit.”
  • Skill — “Every time Claude writes TypeScript in this project, it should follow our specific conventions — but I don’t want to type them every time.”
  • Subagent — “Before I implement this feature, have Claude read the entire legacy module and give me a summary. I don’t need all that detail in my main session.”
  • Piping — “Pipe docker logs into Claude to classify errors without opening an interactive session.”

They are not mutually exclusive. The most powerful patterns combine them:

flowchart TD
    Edit[Claude edits a file] --> Hook[PostToolUse hook fires]
    Hook --> Tests[Run test suite]
    Tests --> Fail{Tests pass?}
    Fail -->|Yes| Continue[Claude continues working]
    Fail -->|No| SA[Spawn arch-explorer subagent]
    SA --> SA2[Subagent reads test output + edited file]
    SA2 --> Fix[Subagent suggests fix]
    Fix --> Main[Main session applies fix]
    Main --> Edit

Getting Started Checklist

  1. Add auto-format hook — low risk, immediate value, no false positives
  2. Add destructive command block — safety net that prevents accidents
  3. Create one custom command — save a prompt you type repeatedly
  4. Try the test-on-edit hook — only in a project with fast tests (<10s)
  5. Create one skill — start with your project’s test conventions
  6. Try one subagent investigation — before implementing a feature, not during
  7. Add isolation: worktree to any subagent that writes files

Start with one. The hook adds immediate safety; the command saves your next repeated task. You do not need all four at once — add each when you feel the friction it solves.

Abhay

Abhay Pratap Singh

DevOps Engineer passionate about automation, cloud infrastructure, and self-hosted tools. I write about Kubernetes, Terraform, DNS, and everything in between.