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 .py files (skip others silently)
  • Run black with no output unless there’s an error
  • Gracefully handle the case where black is not installed
  • Log a single line to stderr so you can see it fired

Steps

  1. Create the hook script at ~/.claude/hooks/auto_format.py
  2. Make it executable: chmod +x ~/.claude/hooks/auto_format.py
  3. Register it in ~/.claude/settings.json under PostToolUse for both
    the Write and Edit matchers
  4. Test it by asking Claude to write a badly-formatted Python file
    (e.g., inconsistent spacing, wrong quotes)
  5. 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/skip

Acceptance 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 black is 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 -rf with a clear explanation
  • Block commands containing DROP TABLE or DELETE FROM without a WHERE clause
  • Block git push --force to main or master
  • Allow all other commands through

Steps

  1. Create the script at ~/.claude/hooks/safety_guard.py
  2. Register it as a PreToolUse hook matching Bash
  3. Write unit tests for each blocked pattern using pytest
  4. Test by asking Claude to run a dangerous command and verifying it gets blocked
  5. 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 through

Exercise 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:

  1. Run git diff HEAD~1 HEAD (or git diff --staged if nothing is committed)
  2. 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
  3. Output the review in a structured format with severity levels
  4. 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 variable

Acceptance 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:

  1. A /ship skill that: runs tests → checks linting → creates a commit → pushes branch → opens a PR
  2. A PostToolUse hook that logs the PR URL when it’s created
  3. A Stop hook that notifies you when the full pipeline completes

Ship Skill Requirements

File: .claude/skills/ship.md

The skill must:

  1. Run pytest (or the project’s test command)
  2. If tests fail: show failures, ask “Should I fix these before shipping?”
  3. If tests pass: run flake8 (or eslint) for linting
  4. If linting passes: stage all changes, generate a conventional commit message, commit
  5. Push the current branch to origin
  6. Open a PR using gh pr create with:
    • A title generated from the commit message
    • A body summarizing the changes
    • Labels: auto-generated if 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

  • /ship skill 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:

  1. 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.

  2. 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?

  3. 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:

  1. 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.

  2. 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:

  1. Your PreToolUse hook is supposed to block rm -rf commands, but it’s blocking ALL
    Bash commands. How do you debug this?

  2. You added a Stop hook 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.