Module 07 Exercises: Skills, Hooks, and Workflow Automation
Work through these exercises in order. Each builds on concepts from the module README.
Estimated total time: 3–5 hours.
Exercise 1: Write a Black Auto-Formatter Hook
Difficulty: Beginner
Time: 30–45 minutes
Goal
Create a PostToolUse hook that automatically runs the black Python formatter on any
Python file that Claude writes or edits. The hook must:
- Only trigger for
.pyfiles (skip others silently) - Run
blackwith no output unless there’s an error - Gracefully handle the case where
blackis not installed - Log a single line to stderr so you can see it fired
Steps
- Create the hook script at
~/.claude/hooks/auto_format.py - Make it executable:
chmod +x ~/.claude/hooks/auto_format.py - Register it in
~/.claude/settings.jsonunderPostToolUsefor both
theWriteandEditmatchers - Test it by asking Claude to write a badly-formatted Python file
(e.g., inconsistent spacing, wrong quotes) - Verify the file is formatted after Claude writes it
Expected Hook Script Structure
#!/usr/bin/env python3
"""
PostToolUse hook: auto-format Python files with black after Write/Edit.
"""
import json
import sys
import subprocess
import shutil
from pathlib import Path
# TODO: Read payload from stdin
# TODO: Extract file_path from tool_input
# TODO: Check if file ends with .py
# TODO: Check if black is installed (use shutil.which)
# TODO: Run black quietly
# TODO: Write one line to stderr indicating success/skipAcceptance Criteria
- Hook script exists and is executable
- Hook is registered in settings.json under both Write and Edit matchers
- Running
/test-black-hook(or asking Claude to write a Python file) triggers formatting - Non-Python files are silently skipped
- If
blackis not installed, hook exits 0 without crashing
Stretch Goal
Extend the hook to also run isort on the file after black. Both tools should run
in sequence. If either fails, log the error but still exit 0 (this is PostToolUse,
not a blocking hook).
Exercise 2: Write a Dangerous Command Blocker Hook
Difficulty: Intermediate
Time: 45–60 minutes
Goal
Create a PreToolUse hook that protects against accidental destructive Bash commands.
The hook must:
- Block commands containing
rm -rfwith a clear explanation - Block commands containing
DROP TABLEorDELETE FROMwithout aWHEREclause - Block
git push --forcetomainormaster - Allow all other commands through
Steps
- Create the script at
~/.claude/hooks/safety_guard.py - Register it as a
PreToolUsehook matchingBash - Write unit tests for each blocked pattern using
pytest - Test by asking Claude to run a dangerous command and verifying it gets blocked
- Verify that safe Bash commands pass through unaffected
Blocked Patterns to Implement
BLOCKED_PATTERNS = [
{
"pattern": r"rm\s+-rf",
"message": "BLOCKED: rm -rf is a destructive recursive deletion. Use a safer alternative or confirm via direct terminal."
},
{
"pattern": r"DROP\s+TABLE",
"message": "BLOCKED: DROP TABLE detected. Database schema changes require manual execution."
},
{
"pattern": r"DELETE\s+FROM\s+\w+\s*;", # DELETE without WHERE
"message": "BLOCKED: DELETE without WHERE clause would delete all rows. Add a WHERE condition."
},
{
"pattern": r"git\s+push\s+.*--force.*\s+(main|master)",
"message": "BLOCKED: Force push to main/master is not allowed via Claude Code."
}
]Acceptance Criteria
- Each blocked pattern is tested with a pytest unit test
- Blocked commands return exit code 1 with a human-readable message
- Safe commands return exit code 0
- The hook handles malformed JSON input gracefully (try/except)
- Pattern matching is case-insensitive where appropriate
Test Cases to Write
def test_blocks_rm_rf():
... # simulate payload with "rm -rf /tmp/test"
def test_blocks_drop_table():
... # simulate payload with "psql -c 'DROP TABLE users'"
def test_allows_safe_git_push():
... # simulate payload with "git push origin feature/my-branch"
def test_blocks_force_push_to_main():
... # simulate payload with "git push --force origin main"
def test_allows_non_bash_tool():
... # simulate a Write tool payload — should pass throughExercise 3: Build a /code-review Skill
Difficulty: Intermediate
Time: 60–90 minutes
Goal
Write a production-ready /code-review skill that Claude can invoke on the current
git diff to perform a structured code review.
Skill Requirements
The skill must instruct Claude to:
- Run
git diff HEAD~1 HEAD(orgit diff --stagedif nothing is committed) - Identify any of the following issues:
- Bugs: off-by-one, null pointer risks, unhandled exceptions
- Security: hardcoded secrets, SQL injection, unvalidated inputs
- Performance: N+1 queries, unnecessary loops, large memory allocations
- Style: inconsistency with existing code patterns
- Output the review in a structured format with severity levels
- Provide a final verdict: APPROVE / REQUEST_CHANGES
Output Format
## Code Review — <branch name> — <date>
### Summary
<2-3 sentences on what the change does>
### Issues Found
| Severity | Location | Issue |
|----------|----------|-------|
| CRITICAL | src/auth.py:42 | Hardcoded API key in source code |
| WARNING | src/db.py:18 | Missing index on frequently queried column |
| INFO | src/api.py:7 | Variable name `x` is unclear; consider `user_count` |
### Security Checklist
- [ ] No hardcoded credentials
- [ ] User inputs are validated/sanitized
- [ ] No SQL injection vectors
- [ ] Dependencies are not outdated
### Verdict
REQUEST_CHANGES
### Required Changes Before Merge
1. Remove hardcoded API key from src/auth.py:42 — move to environment variableAcceptance Criteria
- Skill file is in
.claude/skills/code-review.md - Frontmatter includes name, description, and trigger
- Skill handles the case where there are no changes to review
- Security checklist is always included
- Issue table uses CRITICAL / WARNING / INFO severity levels
- Final verdict is always one of APPROVE / REQUEST_CHANGES
Exercise 4: Build a Complete Automation Workflow
Difficulty: Advanced
Time: 90–120 minutes
Goal
Combine skills + hooks into a complete “test-and-ship” automation pipeline:
- A
/shipskill that: runs tests → checks linting → creates a commit → pushes branch → opens a PR - A
PostToolUsehook that logs the PR URL when it’s created - A
Stophook that notifies you when the full pipeline completes
Ship Skill Requirements
File: .claude/skills/ship.md
The skill must:
- Run
pytest(or the project’s test command) - If tests fail: show failures, ask “Should I fix these before shipping?”
- If tests pass: run
flake8(oreslint) for linting - If linting passes: stage all changes, generate a conventional commit message, commit
- Push the current branch to origin
- Open a PR using
gh pr createwith:- A title generated from the commit message
- A body summarizing the changes
- Labels:
auto-generatedif available
Hook Requirements
PostToolUse hook matching Bash:
- Detect if the bash command output contains a GitHub PR URL (matches
https://github.com/.*/pull/\d+) - If found, extract the URL and append it to
~/.claude/shipped_prs.log - Log format:
[timestamp] [project] [pr_url]
Acceptance Criteria
-
/shipskill exists and follows the step-by-step structure - Skill handles test failures gracefully with user confirmation
- PostToolUse hook correctly extracts PR URLs from bash output
- Shipped PRs log is created at
~/.claude/shipped_prs.log - Stop hook fires a macOS notification with the PR URL
Exercise 5: Interview Simulation
Difficulty: All levels
Time: 20–30 minutes
Goal
Practice answering these questions concisely (target: 60–90 seconds per answer).
Record yourself or write answers without looking at the README.
Questions
Round 1 — Fundamentals:
-
Explain the lifecycle of a Claude Code hook from trigger to effect. Be specific about
what format the payload takes and how exit codes work. -
I have a script that should run after every file Claude edits. Where do I configure
it, what event type do I use, and what matcher do I set? -
What is the difference between a skill file and content in CLAUDE.md? Give a concrete
example of what you’d put in each.
Round 2 — Design:
-
A teammate wants Claude to automatically create a GitHub issue whenever it detects a
TODO comment in code it reviews. Walk me through exactly how you’d implement this
with skills and/or hooks. -
How would you build a system where Claude refuses to delete files in the
src/
directory without first showing you what it will delete and asking for confirmation?
Round 3 — Troubleshooting:
-
Your
PreToolUsehook is supposed to blockrm -rfcommands, but it’s blocking ALL
Bash commands. How do you debug this? -
You added a
Stophook to notify Slack, but notifications are intermittent. What
are three possible causes and how do you diagnose each?
Self-Evaluation Rubric
Rate each answer on three dimensions:
- Correctness (1–3): Did you get the technical facts right?
- Clarity (1–3): Was the explanation clear without jargon overload?
- Completeness (1–3): Did you cover the key points in the time allotted?
Target: 7+/9 on each question before moving to the next module.