Specialists

Automate recurring work

Turn a procedure you've done twice by hand into a skill, then wire one hook so it never gets skipped.

Behrad Mirafshar
Behrad Mirafshar
Founder, Bonanza Design
9 min read

When to take this module

Completing Context Engineering for Claude Code means Core 1–3 plus the takeover test. This module is optional, for one specific situation: a procedure in your own project that you have now done by hand at least twice.

You end with a skill that encodes the procedure, and a hook that checks it never gets skipped, the same shape as the updating-claude-md skill you already ran in Core 2, whose own build is the worked example below.

The lessons

In order. Skill or command first, since the wrong choice there makes everything after it harder than it needs to be.

  1. Skills versus custom commands versus Projects
    A custom command inserts text you already wrote. It adds no new ability, and Claude can't choose to use it. You have to type it. A skill loads by its own description, so Claude picks it up on its own when the situation matches. A Project bundles instructions and files for one workspace, with no pickup logic at all. If the thing you're automating should just happen when the situation calls for it, it's a skill.
  2. Where skills live: user, project, public
    A user-level skill lives in your home folder and follows you into every project. A project-level skill ships inside the repo, so anyone who clones it gets it too. A public skill or plugin comes from someone else's marketplace listing: same shape, different trust level.
  3. Plugins and reviewing before you install
    A plugin bundles skills, commands and hooks someone else wrote. A hook runs shell commands on your machine with your session's permissions the moment it fires. Read the SKILL.md and every hook script before you install, the same way you'd read a package before adding it as a dependency.
  4. Hooks: the nine events you will actually use
    Claude Code has more than 30 hook events, and the list grows with each release. Nine of them cover almost every hook worth writing, from the moment a session starts to the moment it ends. Run /hooks in your own session for the full current list. Pick the event that matches when the thing should happen, not the event that happens to be running when you write the hook.
  5. Wiring a hook in settings.json
    Registering a hook is a few lines in settings.json: which event, which command to run, an optional timeout. Nothing runs until the wiring exists: the skill and the hook script are inert on their own.
  6. Bonus: how updating-claude-md was built
    The skill from Core 2 that already keeps your CLAUDE.md current wasn't handed to you finished. It was built test-first, it failed once in a documented way, and the fix is below: the real build-along, not a description of one.

Hooks: the nine events you will actually use

Every hook you'll ever write fires on one of these. Pick the event that matches when the thing should happen.

The nine hook events worth knowing first, and the one this page's build-along wires. Run /hooks for the full list in your version.
EventFires when
SessionStartA new session begins.
UserPromptSubmitYou submit a prompt, before Claude reads it.
PreToolUseBefore a tool call runs. Can block it.
PostToolUseAfter a tool call finishes.
PermissionRequestClaude Code needs your permission for something.
NotificationClaude Code shows a notification, e.g. a permission prompt.
StopThe main reply finishes.
TaskCompletedA background task finishes.
SessionEndA session ends: the one this page's build-along uses.

We cover what a skill actually is, and where it fits next to a custom command, in more depth on the Claude Skills page.

The assignment

This one runs on your own project. There's no reference-project snapshot for specialists: your repeated procedure stands in for the “add a competitor” step Move from idea to evidence shipped a working page from.

Starts from

Your own finished core project, where "add a competitor" (or your project's equivalent recurring step) has now been done twice by hand.

Required output

A skill encoding that procedure, plus one hook.

Passes when

The skill runs on a third instance and produces the same shape as the two you did by hand. The hook fires at the moment you claimed it would, verified in its log.

The Claude Code skill, the hook, the wiring

This example adapts the updating-claude-md skill from Core 2, its hook and its wiring, in the order you build them: skill first, then the hook that runs it, then the wiring that fires the hook. A reader who writes the hook first has nothing to point it at. Project details use a fictional reading-list app.

1. The skill: how

SKILL.md (excerpt)
---
name: updating-claude-md
description: Use when a session is about to end, when CLAUDE.md has no
  last_updated marker, or when files, dev-docs or sessions may have changed
  since CLAUDE.md was last updated.
---

# Updating CLAUDE.md

## Overview

CLAUDE.md loads into every session, so it holds the gist of the project and
says where the detail lives. The detail stays in its own folder. A marker in
the frontmatter records when CLAUDE.md was last brought up to date, so each
run only has to look at what changed since then.

Write straight to CLAUDE.md. This runs before a session ends, so there is no
one to approve a draft.

## The shape CLAUDE.md always has

```markdown
---
last_updated: 2026-09-14T16:40
---
# <Project name>

## Overview
What it is and who it is for, in 2 to 3 lines.

## Active work
- `dev-docs/2026_09_12-PROGRESS-pt1-Reading_List_App.md`: Build a
  reading-list app that saves books and tracks reading progress.
```

## Common mistakes

| Mistake | Fix |
|---|---|
| Progress numbers or "status as of" lines in CLAUDE.md | Active work lists the dev-doc file name and its gist |
| Field lists, API tables, long explanations copied in | One line and the path of the file that holds them |
| Marker left unchanged | The marker moves every time, even when nothing else changed |
| Only the latest dev-doc read | Every dev-doc not DONE appears in Active work |
| Hand-written guardrails dropped while restructuring | They stay; only their placement changes |

2. The hook: when

Fires on SessionEnd. The four guards are annotated inline. Each one is a real failure the hook had to be corrected for. It uses your configured permissions. Test the skill interactively first and allow only the tools it needs; a background run cannot ask you to approve a blocked action. Check its log for failures.

~/.claude/hooks/claude-md-update.sh
#!/usr/bin/env bash
# SessionEnd hook: when a session ends on purpose in a project that has a
# CLAUDE.md, spawn a headless Claude that runs the updating-claude-md skill.
# Never blocks the exit.
LOG="$HOME/.claude/claude-md-update.log"
[[ "${CLAUDE_MD_UPDATER:-}" == "1" ]] && exit 0   # guard 1: don't recurse
IN=$(cat 2>/dev/null || true)
command -v jq >/dev/null || exit 0
reason=$(jq -r '.reason // ""' <<<"$IN")
sid=$(jq -r '.session_id // ""' <<<"$IN")
cwd=$(jq -r '.cwd // ""' <<<"$IN")
case "$reason" in prompt_input_exit|logout) ;; *) exit 0 ;; esac   # guard 2: only deliberate exits
[[ -z "$cwd" || "$cwd" == "$HOME" || "$cwd" == *"/.claude/plugins/"* ]] && exit 0   # guard 3a
[[ -f "$cwd/CLAUDE.md" ]] || exit 0   # guard 3b: only projects that already have a CLAUDE.md
lock="$HOME/.claude/claude-md-update.lock.$(printf '%s' "$cwd" | md5 -q)"
mkdir "$lock" 2>/dev/null || exit 0   # guard 4: one updater per project at a time
echo "$(date '+%F %T') start cwd=$cwd sid=$sid" >> "$LOG"
(
  cd "$cwd" && CLAUDE_MD_UPDATER=1 claude -p --model sonnet \
    --permission-mode acceptEdits \
    "Use the updating-claude-md skill now and update CLAUDE.md. Do not ask questions." \
    >> "$LOG" 2>&1
  echo "$(date '+%F %T') done cwd=$cwd rc=$?" >> "$LOG"
  rmdir "$lock" 2>/dev/null
) </dev/null >/dev/null 2>&1 &
disown
exit 0

The four guards

  1. Don't recurse.
    The Claude the hook spawns also ends a session, which would fire the hook again, forever. An environment variable set only inside the spawned run breaks the loop.
  2. Only deliberate exits.
    Matches prompt_input_exit and logout specifically. A crash shouldn't trigger a rewrite of a file nobody was ready to have touched.
  3. Only projects that already have a CLAUDE.md.
    Skips the home directory and plugin folders, and skips any project that hasn't been introduced to the skill yet. The hook maintains a file, it doesn't create the first one.
  4. One at a time.
    A lock keyed on the project's path stops two sessions closing at once from overwriting each other's update.

3. The wiring: who does it

Three lines in settings.json register the script against the event. Nothing runs until this exists: the skill and the hook script are inert on their own.

settings.json (excerpt)
"SessionEnd": [
  { "hooks": [{ "type": "command", "command": "~/.claude/hooks/claude-md-update.sh" }] }
]
A hook is when. A skill is how. claude -p is who does it: a second, headless Claude in the background, so you're never waiting.

Skills and hooks are one way to wire a tool to Claude. When the choice is between that and a direct API call, see MCP or a single API call.

The method correcting itself

This is the skill's own documented failure, from when it was first built and tested, not a hypothetical. The first baseline run wrote progress numbers straight into CLAUDE.md, where they went stale the moment the numbers changed.

Before

CLAUDE.md
## Active work

- pt1-Reading_List_App: Book search working. 3 of 5 tests
  passing as of 2026-09-12. Reading-progress filter still failing
  (investigating filter state, ~60% done overall).

Every session that opened this file after that point read a number that was already wrong. The fix wasn't a longer file. It was a rule: point-in-time progress belongs in the dev-doc it describes, never in CLAUDE.md itself. That rule is now the first row of the skill's own Common mistakes table, above.

After

CLAUDE.md
## Active work

- `dev-docs/2026_09_12-PROGRESS-pt1-Reading_List_App.md`: Build a
  reading-list app that saves books and tracks reading progress.
Gist in CLAUDE.md, detail in the file it points to: that's the difference between instructions that stay useful and instructions nobody reads.

Turn a job you repeat into a skill

A blank skill file to fill in, plus a one-page card of the nine hook events you will actually use, so the skill runs without you starting it.

A procedure in a document

“At the end of a session, update the project notes: read the work records, keep the gist, leave the detail in its own file.”

It only works when someone remembers to open the document, and Claude cannot pick it up on its own.

The same procedure as a skill

A skill is how · a hook is when
SKILL.md, plus the line that fires it
The description line“Use when a session is about to end, or when files may have changed since the notes were last updated.” Claude reads that line and picks the skill up without being told.
The stepsRead the marker, collect what changed since, write the gist into the notes, move the marker.
The hookThree lines in settings.json fire it the moment a session ends, in the background, so nobody waits.
The guardsDo not fire on a crash, do not run twice at once, skip folders with no notes, and never let the spawned session trigger itself.

Unlock all course materials

One email unlocks every material in the course. Verify, and they open in that browser.

FAQ

It writes one file, so it runs with --permission-mode acceptEdits. That is the whole grant: it is not allowed to do anything else, and it never asks you a question, because nobody is at the keyboard once a session has ended. If your own setup restricts writes further, allow CLAUDE.md for that project, or the run finishes having changed nothing and says so in its log.

No. Start with the frontmatter (a name and a description of when to use it) and one clear step. Run it once, fix what's wrong, add the next step. The updating-claude-md skill on this page went through the same process.

A custom command only runs when you type it, and it just inserts text you already wrote. No new ability. A skill loads by its own description, so Claude can pick it up on its own when the situation matches, without you naming it.

Read the SKILL.md and every hook script first. A hook runs shell commands on your machine with your session's permissions the moment its event fires. Treat it the way you'd treat a new dependency, not the way you'd treat a text snippet.

SessionEnd: the same event this page's build-along wires. Guard it so it only fires on a deliberate exit, not a crash.

Not if it's backgrounded. The updating-claude-md hook spawns a second, headless Claude with claude -p and disowns it. The session you're in has already exited by the time it runs.

Fires twice usually means two sessions closing at once: add a lock keyed on the project path, guard 4 above. Never fires usually means the event name in settings.json doesn't match an event your version supports (run /hooks to see the list), or the command path is wrong; check the hook's own log file first.

Want your own recurring work automated, not just read about?

A working session that turns your team's repeated procedures into skills and hooks.

Book the workshopLearn more