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 isis_error: truewith 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)
0
Public
Reinvent the Wheel
handmade-claude-code-4-skills
30 min
~12 per session
No
10–40
- ai-agent
- harness
- skills
- handmade-claude-code
- campaign
1
Set up and carry parts one to three forward
+10 pts per passing check · +10 for completing the task
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:andtest:. 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,aideror any other coding agent,
or building on an agent SDK that owns the loop, is not building one.Judged by
The Debrief 2
Skills are listed
+20 pts per passing check · +10 for completing the task
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.mdwith anameand adescriptionin its
frontmatter is listed in the system prompt — name and description, so
the model can decide whether a skill applies — and a tool namedskill
is advertised alongside the others.Judged by
Architecture 3
A skill is a tool
+30 pts per passing check · +10 for completing the task
30
pts / check
+30 pts per passing check · +10 for completing the task
skillwith{"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 canreadthe
files that live beside SKILL.md. A name that matches no skill is an
error result, and the loop goes on.Judged by
Code Quality 4
Slash commands
+20 pts per passing check · +10 for completing the task
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>.mdas the user message, with every$ARGUMENTSreplaced 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
DX Review 5
A hook can say no
+30 pts per passing check · +10 for completing the task
30
pts / check
+30 pts per passing check · +10 for completing the task
Before a tool runs, a
PreToolUsehook 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 isis_error: truewith the hook's stderr as content.6
A hook can add a note
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
After a tool runs, a
PostToolUsehook sees what happened — the same
JSON, plustool_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
Code Quality 7
Prompt and stop hooks
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Two more moments a hook can watch.
UserPromptSubmitruns when a prompt
comes in, with thepromptin its JSON; whatever it prints is appended
to the user message the model receives.Stopruns once the final
answer is ready, before the agent exits — the agent waits for it.Judged by
Technical Governance 8
Settings layer
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Two settings files, one view.
.agent/settings.local.jsonsits 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 per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
The model can delegate. Every
.agent/agents/<name>.mdis listed in
the system prompt by name and description, and a tooltaskwith{"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).