Index Ai Academy
Phase 10 · Agent harnesses · Context & control

Hooks and skills

Extend a harness at lifecycle events, and load know-how on demand.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. Hooks: code at lifecycle events
  2. A hook that blocks migrations
  3. Skills: know-how on demand
  4. What progressive disclosure saves
  5. Instructions, skills, hooks, tools
  6. Reading a skill header
  7. 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:

eventwhentypical uses
SessionStarta session beginsload project context, check the environment
UserPromptSubmitthe person sends a messageadd context, block secrets from being sent
PreToolUsebefore a tool runsblock risky calls, change inputs, auto-approve safe ones
PostToolUseafter a tool runsrun a formatter or linter, give the model feedback
Stopthe agent is about to finishcheck 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.

Worked example

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)
It runs right here in your browser, with nothing to install.

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.

Quick check
What does this print?
def pre_tool_use(command):
    return "deny" if "rm -rf" in command else "allow"
print(pre_tool_use("rm -rf /"))
  1. deny
  2. None
  3. rm -rf /
  4. allow
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.

Quick check
A team must never let the agent edit files under migrations/. What's the most reliable way?
  1. Asking the model at the start of each session
  2. A PostToolUse hook that undoes the edit
  3. A sentence in the system prompt asking it not to
  4. A PreToolUse hook (or deny rule) that blocks edits to that path
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:

  1. Always: only each skill's name and description go in the system prompt. A hundred skills cost a few thousand tokens.
  2. When relevant: the model decides a skill applies and loads its SKILL.md.
  3. 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.

Worked example

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")
In the lesson you can change this and run it.

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:

pieceloadedgood for
project instructions (CLAUDE.md, AGENTS.md)every sessionconventions everyone needs: build commands, style
skillson demandtask know-how: releases, reports, a file format
hooksrun by the harnessrules that must always hold
tools and MCP serversdefinitions each callactions and live data

Harnesses often bundle these into plugins: one install adding skills, hooks, commands and MCP servers together.

Try it now

Prompts ask, and hooks enforce.

Quick check
Where should a team put its build and test commands so every agent session knows them?
  1. In a skill that loads on demand
  2. In each task's prompt
  3. In the project instructions file, such as CLAUDE.md or AGENTS.md
  4. In a PreToolUse hook
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.

Worked example

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")
In the lesson you can change this and run it.

The harness puts the name and description in the catalog. The body stays on disk until the model asks for it.

Quick check
A skill's description says only "PDF tool." What's the problem?
  1. It's too long
  2. Skills can't handle PDFs
  3. The model can't tell when to load it; descriptions should say when the skill applies
  4. Nothing
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

Create a free account

Try a quiz question

Which hook event fits "run the code formatter after every edit"?

  1. PostToolUse
  2. PreToolUse
  3. SessionStart
  4. UserPromptSubmit

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson