Hooks and skills
Extend a harness at lifecycle events, and load know-how on demand.
Create a free account Try part of it here
Why it matters
No harness ships with everything each team needs. One team wants code formatted after every edit; another must block changes to migration files; a third wants the agent to know its release process. Writing these into the system prompt is unreliable and fills the context. Harnesses offer two better extension points: hooks, for guaranteed behavior at set moments, and skills, for know-how loaded on demand.
What you'll be able to do
- Explain hooks: code the harness runs at lifecycle events such as before and after each tool
- Use hooks to block, change or add to what an agent does, with guarantees prompts can't give
- Explain skills and progressive disclosure: load know-how only when a task needs it
- Build a hook bus and a skill catalog
What it covers
- Hooks: code at lifecycle events
- A hook that blocks migrations
- Skills: know-how on demand
- What progressive disclosure saves
- Instructions, skills, hooks, tools
- Reading a skill header
- When a safety hook crashes
Hooks: code at lifecycle events
A smoke alarm goes off whether or not anyone remembers to check for smoke. A hook works the same way: it's a function or script the harness runs when something happens. These moments are called lifecycle events. Claude Code's events include:
| event | when | typical uses |
|---|---|---|
SessionStart | a session begins | load project context, check the environment |
UserPromptSubmit | the person sends a message | add context, block secrets from being sent |
PreToolUse | before a tool runs | block risky calls, change inputs, auto-approve safe ones |
PostToolUse | after a tool runs | run a formatter or linter, give the model feedback |
Stop | the agent is about to finish | check the tests pass; if not, tell it to keep going |
In Claude Code, hooks are shell commands. The harness sends the event as JSON on stdin. The hook answers with an exit code or JSON: exit code 0 means carry on, and exit code 2 means block, with the message on stderr passed to the model.
Why hooks, not instructions? "Always run the formatter after editing" in a system prompt is a request the model may forget. A PostToolUse hook makes it happen every time. Use prompts for judgment and hooks for rules.
Hooks are code you run. They run with your permissions on every event, so review them like any other code. Give each a timeout. And decide what happens when a safety hook crashes. A check that guards against damage should fail closed, like a door that stays locked when the power goes out: it blocks the action rather than quietly allowing it.
A hook that blocks migrations
A hook reads the event as JSON and answers with an exit code. Here it is as a function, so you can see what happens for each call.
import json def pre_tool_use(event_json): event = json.loads(event_json) path = event["tool_input"].get("file_path", "") if event["tool_name"] in {"Edit", "Write"} and path.startswith("migrations/"): return 2, "Don't edit migrations. Add a new one instead." return 0, "" calls = [ {"tool_name": "Edit", "tool_input": {"file_path": "src/app.py"}}, {"tool_name": "Edit", "tool_input": {"file_path": "migrations/0007.sql"}}, {"tool_name": "Read", "tool_input": {"file_path": "migrations/0007.sql"}}, ] for call in calls: code, message = pre_tool_use(json.dumps(call)) print(call["tool_name"], call["tool_input"]["file_path"], "->", code, message)
Exit code 2 blocks the call, and the message goes to the model, which can adjust. Reading a migration is still allowed. No prompt, however firm, gives you this certainty, because here the harness enforces the rule.
def pre_tool_use(command): return "deny" if "rm -rf" in command else "allow" print(pre_tool_use("rm -rf /"))
Show the answer
deny
"rm -rf" is in the command, so the function returns deny. A hook is code, so this rule holds whatever the model decides.
migrations/. What's the most reliable way?Show the answer
A PreToolUse hook (or deny rule) that blocks edits to that path
A hook or rule runs on every call, whatever the model remembers. Undoing after the fact is weaker than not doing it.
Skills: know-how on demand
Picture a box of recipe cards. You can read every tab at a glance, you pull out a card when you need it, and you only fetch special ingredients if the card calls for them.
A skill is one of those cards: a folder that teaches the agent how to do one kind of task. Its core is a SKILL.md file: a short header with a name and a description, then instructions. It can include scripts, templates and reference files.
release-notes/ ├── SKILL.md (name, description, then steps) ├── template.md └── scripts/collect_changes.py
The trick is progressive disclosure, which means revealing more only as it's needed, in three levels:
- Always: only each skill's name and description go in the system prompt. A hundred skills cost a few thousand tokens.
- When relevant: the model decides a skill applies and loads its
SKILL.md. - When needed: it opens the skill's extra files or runs its scripts only if a step calls for it.
So an agent can have deep knowledge of many tasks without paying for all of it on every call. The description does the routing, so it must say clearly when to use the skill.
What progressive disclosure saves
Only names and descriptions load up front. Here's the arithmetic for 100 skills, with one of them used in a session.
skills = 100 header = 40 # tokens for a name and description body = 1500 # tokens for a whole SKILL.md used = 1 all_loaded = skills * (header + body) progressive = skills * header + used * body print("load everything:", all_loaded, "tokens on every call") print("progressive:", progressive, "tokens") print("saved:", round(100 * (1 - progressive / all_loaded)), "percent")
That's why a team can have a hundred skills. It also shows why the description matters so much: those 40 tokens are all the model sees when it's deciding whether to open the rest.
Instructions, skills, hooks, tools
Think of a restaurant. The staff handbook applies every day, the recipe box comes out when needed, nobody has to watch the smoke alarm, and the oven does the actual work. A harness has the same four kinds of thing:
| piece | loaded | good for |
|---|---|---|
project instructions (CLAUDE.md, AGENTS.md) | every session | conventions everyone needs: build commands, style |
| skills | on demand | task know-how: releases, reports, a file format |
| hooks | run by the harness | rules that must always hold |
| tools and MCP servers | definitions each call | actions and live data |
Harnesses often bundle these into plugins: one install adding skills, hooks, commands and MCP servers together.
Prompts ask, and hooks enforce.
Show the answer
In the project instructions file, such as CLAUDE.md or AGENTS.md
Every session needs them, so they belong in the file that loads every time. Skills are for know-how only some tasks need.
Reading a skill header
SKILL.md starts with a small block between --- lines.
text = """--- name: release-notes description: Write release notes from merged changes. Use when asked for a changelog or release notes. --- 1. Run scripts/collect_changes.py to list merged changes. 2. Group them as Added, Changed and Fixed. """ _, header, body = text.split("---\n", 2) fields = dict(line.split(": ", 1) for line in header.strip().split("\n")) print(fields["name"]) print(fields["description"]) print(len(body.split()), "words in the body, loaded only when needed")
The harness puts the name and description in the catalog. The body stays on disk until the model asks for it.
Show the answer
The model can't tell when to load it; descriptions should say when the skill applies
Under progressive disclosure, the description is all the model sees until it loads the skill.
When a safety hook crashes
Decide this before it happens. Say your hook blocks edits to migrations/, and it crashes on a strange input. If a crash means "carry on", then your protection vanishes exactly when something unusual is going on. So a safety check should fail closed: if it can't decide, it says no. A convenience hook, like a formatter, can fail open, since a missed format does no harm. Each hook should know which kind it is.
Writing a skill description that gets used
Since the description is all the model sees up front, it decides whether the skill ever runs. Compare these:
- "PDF tool." It says what, and not when.
- "Extract text and tables from PDF files. Use when the user mentions a PDF, or asks to read a scanned document or invoice."
The second names the trigger, in the words a user would really say. If a skill isn't being used when you expect, fix its description before you touch anything else.
Where each thing belongs
A useful test is what happens if the model forgets. If forgetting is merely annoying, an instruction will do. If forgetting would cause damage, it has to be a hook, since prompts are requests and hooks are enforced by code. And if something is long and rarely needed, it belongs in a skill, so that it doesn't cost tokens on every call. Teams that put everything in one huge instruction file pay for all of it each turn, and the model pays less attention to each part as it grows.
In plain words: forgetting is annoying, write an instruction. Forgetting is dangerous, write a hook. Rarely needed, write a skill.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
Which hook event fits "run the code formatter after every edit"?