Skills, Hooks & Custom Commands
Teaching Claude YOUR workflow
Objective
Deep dive into Claude Code's extensibility system.
Deliverable
At least 2 custom skills (one forked, one inline), 3 hooks (command, prompt, agent types), and 1 custom command.
Topics
- Skills: SKILL.md files for project patterns
- Skill frontmatter: name, description, allowed-tools, context, agent, model, hooks
- How skills auto-invoke based on context matching
- Custom slash commands via .claude/commands/
- Hooks: 14 events covering the full Claude Code lifecycle
- Three hook types: command, prompt, agent
- Hook decision control: exit codes, JSON decision patterns
- Async hooks for non-blocking background execution
Activities
- Create a custom skill for your project with supporting files
- Create a skill with `context: fork` that runs in an Explore subagent
- Write a `PreToolUse` command hook that validates Bash commands before execution
- Write a `Stop` prompt hook that checks if all tasks are complete before Claude stops
- Create a custom /deploy command with `disable-model-invocation: true`
- Test skill auto-invocation and argument passing
- Build an async `PostToolUse` hook that runs tests in the background after file edits
- Use dynamic context injection in a PR summary skill
Skills You'll Gain
Skills system, hooks (all 3 types), custom commands, workflow automation, skill forking
Learning Objectives
By the end of this week, you will be able to:
- Create custom skills with SKILL.md files that automate repetitive project workflows
- Explain the three hook types (command, prompt, agent) and when to use each
- Write hooks that validate, log, and control Claude Code's behavior
- Build a custom slash command for common project operations
- Use advanced skill features like forked context and dynamic injection
Lesson
Why Extensibility Matters
So far, you have used Claude Code's built-in features. But every project and every developer has unique workflows. Maybe you always want tests to run after editing a file. Maybe you need Claude to follow specific coding standards. Maybe you want a one-command deploy workflow.
Skills, hooks, and custom commands let you teach Claude Code your specific workflow — turning it from a general-purpose assistant into a custom tool built for your project.
Skills: Teaching Claude Project Patterns
A skill is a SKILL.md file that tells Claude how to handle a specific type of task. Skills live in your project's .claude/skills/ directory.
Think of skills as recipe cards for Claude. Instead of explaining your deployment process every time, you write a skill once and Claude follows it automatically.
Basic skill example (.claude/skills/deploy.md):
---
name: deploy
description: Deploy the application to production
allowed-tools: ["Bash", "Read"]
---
# Deploy Workflow
1. Run `npm run build` and verify no errors
2. Run `npm test` and verify all tests pass
3. Run `npx vercel deploy --prod`
4. Verify the deployment URL is accessible
Now you can type /deploy and Claude follows these exact steps.
Skill frontmatter (the section between --- marks) controls behavior:
| Field | What It Does |
|---|---|
name | The slash command name (e.g., /deploy) |
description | What the skill does (shown in /help) |
allowed-tools | Which tools Claude can use (limits scope) |
context: fork | Run in a separate subagent (isolated context) |
model | Which AI model to use for this skill |
disable-model-invocation | If true, Claude cannot trigger this skill itself — only you can |
user-invocable | If false, only Claude can trigger it (not you) |
Auto-invocation: Skills can match context automatically. If you describe a skill well, Claude recognizes when to use it without you typing the slash command. For example, a skill named "code-review" with description "Review code for quality and bugs" might activate when you say "review my code."
Dynamic context injection: Use backtick-wrapped shell commands to inject live data into skills:
# PR Summary Skill
Current PR diff:
!`gh pr diff`
Summarize the changes above.
The !gh pr diff`` runs before Claude sees the skill, replacing itself with the actual output.
Hooks: Automating Claude's Lifecycle
Hooks are event handlers — code that runs automatically when specific things happen in Claude Code. Think of them as tripwires: when Claude is about to do something (or just did something), your hook fires.
The 14 hook events:
| Event | When It Fires | Can Block? |
|---|---|---|
SessionStart | Session begins or resumes | No |
UserPromptSubmit | You submit a prompt | Yes |
PreToolUse | Before a tool call executes | Yes |
PermissionRequest | When a permission dialog appears | Yes |
PostToolUse | After a tool call succeeds | No |
PostToolUseFailure | After a tool call fails | No |
Notification | When Claude sends a notification | No |
SubagentStart | When a subagent is spawned | No |
SubagentStop | When a subagent finishes | Yes |
Stop | When Claude finishes responding | Yes |
TeammateIdle | When a teammate is about to go idle | Yes |
TaskCompleted | When a task is marked completed | Yes |
PreCompact | Before context compaction | No |
SessionEnd | When a session terminates | No |
Three Hook Types
| Type | How It Works | Use When |
|---|---|---|
command | Runs a shell script. Exit 0 = proceed, exit 2 = block. | Deterministic rules (linting, logging, validation) |
prompt | Sends a prompt to a Claude model for yes/no judgment. | Decisions requiring judgment |
agent | Spawns a subagent that can read files and run tools (up to 50 turns). | Verification requiring codebase inspection |
Command hook example — Block dangerous Bash commands:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.command' | grep -q 'rm -rf' && exit 2 || exit 0"
}]
}]
}
}
This hook fires before every Bash command. It checks if the command contains rm -rf. If yes, it blocks (exit 2). If no, it proceeds (exit 0).
Prompt hook example — Check task completeness before stopping:
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains\"}."
}]
}]
}
}
Agent hook example — Verify tests pass before stopping:
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "agent",
"prompt": "Run the test suite and verify all tests pass. $ARGUMENTS",
"timeout": 120
}]
}]
}
}
Advanced Hook Features:
- Async hooks: Set
"async": trueto run in the background without blocking Claude - Matchers: Regex patterns that filter when hooks fire (e.g.,
Bash,Edit|Write) /hooksmenu: Type/hooksto interactively manage hooks- Stop hook loop prevention: Check
stop_hook_activefield to prevent infinite loops
Custom Slash Commands
For simpler automation, create custom commands in .claude/commands/:
<!-- .claude/commands/test-all.md -->
Run the full test suite with `npm test`. If any tests fail,
analyze the failures and suggest fixes. Show a summary of
pass/fail counts at the end.
Now typing /test-all runs this command. Arguments are available via $ARGUMENTS, $0, $1, etc.
Practice Exercises
Exercise 1 (Guided): Create Your First Skill
- Create the directory:
mkdir -p .claude/skills - Create
.claude/skills/deploy.mdwith:
---
name: deploy
description: Build, test, and deploy to Vercel
allowed-tools: ["Bash", "Read"]
disable-model-invocation: true
---
# Deploy to Production
Follow these steps exactly:
1. Run `npm run build` — stop if there are errors
2. Run `npm test` — stop if any tests fail
3. Run `npx vercel deploy --prod`
4. Report the deployment URL
- Test it by typing
/deployin a Claude Code session
Verification: Typing /deploy triggers the full build-test-deploy workflow. The deployment URL is reported at the end.
Exercise 2 (Independent): Write Three Hooks
Goal: Create one hook of each type:
- Command hook — Log every Bash command to a file (
echo "$command" >> .claude/command-log.txt) - Prompt hook — Before stopping, ask Claude to verify the code is properly formatted
- Agent hook — After file edits, verify the project still builds successfully
Add these to .claude/settings.json.
Hints:
- Start with the command hook (simplest)
- Use
/hooksto verify they are registered - Test each hook by triggering the relevant event
Verification: After a session, .claude/command-log.txt contains logged commands. The prompt hook prevents stopping if code is not formatted. The agent hook catches build errors.
Exercise 3 (Challenge): Advanced Skill with Forked Context
Create a skill that:
- Uses
context: forkto run in an isolated subagent - Injects dynamic context with
!command`` syntax - Has supporting files (a template or example file)
- Accepts arguments via
$ARGUMENTS
Example: A "pr-summary" skill that generates a PR description from the current Git diff.
Self-Assessment Quiz
1. What is a skill in Claude Code, and how is it different from a custom command?
2. Name the three hook types and when you would use each one.
3. What does exit 2 mean in a command hook?
4. What does context: fork do in a skill's frontmatter?
5. How do you prevent a Stop hook from creating an infinite loop?
Answers:
A skill is a SKILL.md file with frontmatter that defines a reusable workflow. It can auto-invoke based on context, control which tools are available, and run in forked subagents. A custom command is a simpler
.mdfile in.claude/commands/that defines a prompt template. Skills are more powerful — they supersede custom commands.Command hooks run a shell script for deterministic rules (logging, validation). Prompt hooks send a prompt to a Claude model for judgment-based decisions. Agent hooks spawn a subagent that can read files and run tools for complex verification.
Exit code 2 in a command hook means "block this action." Exit 0 means "allow it to proceed."
context: forkruns the skill in an isolated subagent with its own context window, keeping the main conversation context clean.Check the
stop_hook_activefield in the event data. If it istrue, exit 0 immediately — this means the Stop hook already fired and you should let Claude stop to avoid an infinite loop.
Skills, Hooks & Commands Updates (Feb 2026)
- Custom command argument shorthand: Use
$0,$1, etc. to access individual arguments in custom commands (v2.1.19) CLAUDE_CODE_ENABLE_TASKSenv var: Set tofalseto revert to the old task system temporarily (v2.1.19)TeammateIdleandTaskCompletedhook events added for multi-agent workflows (v2.1.33)- Bash permission matching fixed for commands using environment variable wrappers (v2.1.38)
- Sandbox bypass fix: Commands excluded from sandboxing via
sandbox.excludedCommandsordangerouslyDisableSandboxno longer bypass the Bash ask permission rule whenautoAllowBashIfSandboxedis enabled (v2.1.34)
Non-Interactive Mode & CI Automation
- Structured outputs in non-interactive (
-p) mode are now fully supported (v2.1.22). Use-pwith--output-format jsonto get structured responses for scripts and CI pipelines. - Startup performance improved when resuming sessions with
saved_hook_context(v2.1.29). - Gateway compatibility: Users on Bedrock or Vertex can set
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1to avoid beta header validation errors (v2.1.25, v2.1.27).
Exercise:
- Run
claude -p "list all files" --output-format jsonand parse the structured output in a bash script - Set up a CI step that uses Claude Code in non-interactive mode
Auto Memory (Feb 2026)
As of v2.1.32, Claude Code automatically records and recalls memories as it works. This is a significant change to how context persists across sessions.
How it works:
- Claude maintains a persistent memory directory at
~/.claude/projects/<project>/memory/ MEMORY.mdis loaded into the system prompt each session (keep it under 200 lines)- Separate topic files (e.g.,
debugging.md,patterns.md) can hold detailed notes - Memories persist across conversations automatically
What Claude saves:
- Stable patterns confirmed across multiple interactions
- Key architectural decisions and important file paths
- User preferences for workflow and tools
- Solutions to recurring problems
What Claude does NOT save:
- Session-specific or temporary state
- Unverified conclusions from a single file read
- Anything duplicating CLAUDE.md instructions
Exercise: After a few sessions on a project, inspect ~/.claude/projects/ to see what Claude has remembered. Try asking Claude to "remember" a preference (e.g., "always use bun instead of npm") and verify it persists in the next session.