Handmade Claude Code 4/7 — Skills, Hooks and Agents

Part four of the Handmade Claude Code campaign: the agent becomes extensible without being recompiled. Skills the model can pull in when it needs them, commands the user can type, hooks that run around every tool call and can veto it, settings that layer, and subagents — whole loops the model can delegate to.

This session continues the agent you built in parts one to three, in the same repository. Everything lives under .agent/ in the working directory.

Skills

.agent/skills/<name>/SKILL.md
---
name: <name>
description: <one line>
---
<the instructions>

Every skill is listed in the system prompt by name and description, so the model knows what it could ask for. A tool skill with input {"name": "<name>"} returns the skill's body (the frontmatter stripped) and the absolute path of the skill's directory, so the model can read the files beside it. An unknown name is an error result.

Slash commands

A prompt that starts with / is a command. /<name> <args> looks for .agent/commands/<name>.md and sends its contents as the user message, with $ARGUMENTS replaced by <args>; when there is no such file but a skill of that name, the skill's body is sent the same way.

Hooks

.agent/settings.json
{"hooks": {"PreToolUse": [{"matcher": "bash", "command": "sh .agent/hooks/pre.sh"}]}}

Events: PreToolUse, PostToolUse, UserPromptSubmit, Stop. A hook is a shell command run with sh -c in the working directory, with one JSON object on stdin: hook_event_name, and for tool events tool_name and tool_input (plus tool_response — the result content — after the tool ran), for UserPromptSubmit the prompt. matcher is a glob over the tool name; absent, the hook runs for every tool.

  • PreToolUse — exit 2 blocks the call: the tool does not run and the result is is_error: true with the hook's stderr as content.
  • PostToolUse — whatever it prints on stdout is appended to the tool result the model sees.
  • UserPromptSubmit — whatever it prints is appended to the user message.
  • Stop — runs once the final answer is ready, before the agent exits.

Settings layering

.agent/settings.local.json sits on top of .agent/settings.json: lists (allow, deny, each hook event) concatenate, scalars from the local file win.

Subagents

.agent/agents/<name>.md
---
name: <name>
description: <one line>
---
<that agent's system prompt>

Every agent is listed in the system prompt by name and description. A tool task with input {"agent": "<name>", "prompt": "<text>"} runs a fresh loop: its own conversation starting with just that prompt, the agent's body as its system prompt (plus your usual environment), the same model command, the same tools. Its final text is the tool result. The parent conversation never sees the child's turns.

The judges

The quality panel — architecture, performance, code quality, test quality, technical governance and DX review — sits on the rungs where its subject is decided, one verdict per judge per rung, on top of the rung's own points. There is no closing review: what you build is judged as you build it, and a rung you never reach is a verdict you never get.

The ladder

  • Set up and carry parts one to three forward (10)
  • Skills are listed (20)
  • A skill is a tool (30)
  • Slash commands (20)
  • A hook can say no (30)
  • A hook can add a note (20)
  • Prompt and stop hooks (20)
  • Settings layer (20)
  • Subagents (40)
Sessions

0

Visibility

Public

Category

Reinvent the Wheel

Slug

handmade-claude-code-4-skills

Duration

30 min

Judge reviews

~12 per session

Active session

No

Points

10–40

Tags
  • ai-agent
  • harness
  • skills
  • handmade-claude-code
  • campaign
  • 1

    Set up and carry parts one to three forward

    10

    pts / check

    +10 pts per passing check · +10 for completing the task

    This part continues your agent. Same repository, same command, same
    model protocol, same tools, same context — what grows is what can be
    plugged in without touching the code: skills, commands, hooks, layered
    settings and subagents, all under .agent/ in the working directory.

    agent: started as: -C

    [--yes] -p ""

    Your AGENTS.md (or README.md) must still carry the two lines the
    platform captures into session memory: agent: and test:. If you are
    starting in an empty folder, your earlier work is fetched for you —
    check that the lines are there and that the agent still builds and
    runs.

    The first rung re-checks what the earlier parts earned: an AGENTS.md in
    the system prompt and a bash round trip.

    Wrapping claude, codex, gemini, aider or any other coding agent,
    or building on an agent SDK that owns the loop, is not building one.

    Judged by
  • 2

    Skills are listed

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    The model should know what it could ask for. Every
    .agent/skills/<name>/SKILL.md with a name and a description in its
    frontmatter is listed in the system prompt — name and description, so
    the model can decide whether a skill applies — and a tool named skill
    is advertised alongside the others.

    Judged by
  • 3

    A skill is a tool

    30

    pts / check

    +30 pts per passing check · +10 for completing the task

    skill with {"name": "<name>"} returns the skill's body — the
    Markdown below the frontmatter, the frontmatter itself stripped — and
    the absolute path of the skill's directory, so the model can read the
    files that live beside SKILL.md. A name that matches no skill is an
    error result, and the loop goes on.

    Judged by
  • 4

    Slash commands

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    A prompt that starts with / is a command. /<name> <args> sends the
    contents of .agent/commands/<name>.md as the user message, with every
    $ARGUMENTS replaced by <args>. When there is no such file but a skill
    of that name exists, the skill's body is sent instead, the same way.

    Judged by
  • 5

    A hook can say no

    30

    pts / check

    +30 pts per passing check · +10 for completing the task

    Before a tool runs, a PreToolUse hook may look at it:

    {"hooks": {"PreToolUse": [{"matcher": "bash", "command": "sh .agent/hooks/pre.sh"}]}}

    The hook runs in the working directory with one JSON object on stdin —
    hook_event_name, tool_name, tool_input. Exit 0 lets the call
    through. Exit 2 blocks it: the tool does not run, and the result the
    model sees is is_error: true with the hook's stderr as content.

  • 6

    A hook can add a note

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    After a tool runs, a PostToolUse hook sees what happened — the same
    JSON, plus tool_response, the result content — and whatever it prints
    on stdout is appended to the result before the model sees it. A linter
    that runs after every edit is one hook away.

    Judged by
  • 7

    Prompt and stop hooks

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    Two more moments a hook can watch. UserPromptSubmit runs when a prompt
    comes in, with the prompt in its JSON; whatever it prints is appended
    to the user message the model receives. Stop runs once the final
    answer is ready, before the agent exits — the agent waits for it.

  • 8

    Settings layer

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    Two settings files, one view. .agent/settings.local.json sits on top
    of .agent/settings.json: lists — allow, deny, the hooks of each
    event — concatenate, and a scalar in the local file wins over the same
    scalar in the shared one. A rule from either file is a rule.

  • 9

    Subagents

    40

    pts / check

    +40 pts per passing check · +10 for completing the task

    The model can delegate. Every .agent/agents/<name>.md is listed in
    the system prompt by name and description, and a tool task with
    {"agent": "<name>", "prompt": "<text>"} runs a fresh loop for it: a
    conversation that starts with just that prompt, the agent file's body
    as its system prompt, the same model command and tools. The child's
    final text is the tool result; the parent never sees the child's turns,
    and the child never sees the parent's.

    The scripted model serves both loops, by call order: the parent asks
    (call 1), the child answers (call 2), the parent finishes (call 3).