Skip to main content
  1. Posts/

Hooks are guarantees, skills are knowledge, subagents are other people.

··1056 words·5 mins·
Nick Liu
Author
Nick Liu
Building infrastructure for Facebook Feed Ranking at Meta. Previously at Walmart, Twitter, AWS, and eBay. MS in Computer Science at Georgia Tech.
Table of Contents
Taming Claude Code Sessions - This article is part of a series.
Part 6: This Article
My Claude Code config now holds two hooks, ten skills, and three custom subagents, and most of them started life in the wrong layer. The instruction the model followed nine times out of ten lived in a prompt until I accepted that nine out of ten is a coin I lose every day. The workflow I pasted into chats became a skill. The bulk work that was draining my priciest model's quota became a fleet of cheaper agents. Same features, different failure modes.
Tested with Claude Code 2.1.x 路 macOS

The four layers
#

Claude Code has four extension points, and they answer four different questions:

LayerQuestion it answersWho decides it runs
CLAUDE.mdWhat should the model always know here?Nobody; it is always in context
HookWhat must happen every time, no exceptions?Your shell script, deterministically
SkillHow do we do X around here?The model, when it judges X is relevant
SubagentWhat deserves its own context, tools, or model?The model delegates, or you ask it to

The column that matters is the last one. Every misplacement I have made comes down to ignoring who actually decides whether the thing runs.

Hooks: when it must happen every time
#

A hook is a script that fires on an event: session start, before a tool call, after one. No model judgment anywhere in the path. That makes it the only layer that can make promises.

My test case was session naming. Part 2 of this series covers the hook itself; the short version is that I wanted every session titled by project and branch so I could find it again. As a habit, it decayed. As a prompt instruction, it worked most of the time, and “most of the time” is exactly the property that makes a pile of unnamed sessions. As a SessionStart hook, it has run on every session since, including the ones launched over SSH and from tools that never show me a prompt at all.

The rule that fell out of this: if you would be upset by one missed execution, it cannot rely on model judgment. Naming, notifications, permission gates, lint-before-commit. Hook territory, all of it.

Skills: when it is knowledge, not a guarantee
#

A skill is a folder of instructions the model loads when the task looks relevant. It is the right home for how-to knowledge that would bloat CLAUDE.md if it sat in context permanently.

The clearest example in my setup is this blog. Writing conventions, shortcode rules, a sourcing policy, a pre-publish checklist: around two hundred lines. They matter only when writing a post, which is a small fraction of my sessions. As a skill, those lines cost nothing until the moment they are needed. My dotfiles repo went through the same migration; its CLAUDE.md keeps a short list of hard rules and points at six skills for the detailed workflows.

The trap runs the other way. A skill triggers on the model’s judgment of relevance, which means a skill is a suggestion with good packaging. When I catch myself writing MUST in a skill description, that is usually the tell that the thing belongs in a hook instead.

Subagents: when the work needs its own context, or a cheaper model
#

A subagent runs in a separate context window, with its own tool permissions and, since this is the part that changed my usage bill, its own model.

I run the main loop on the most capable model available and kept burning through its usage limits, because that model was also doing the grep runs, the batch edits, and the checklist verification. So I defined three agents in ~/.claude/agents/: a bulk worker and a scout on a mid-tier model, and a verifier on the cheapest one. Per Anthropic’s model pricing, the cheapest tier costs a tenth of the top one per token. An eight-point pre-publish check on this very post ran on the cheap verifier; the transcript that check produced never touched my main context window either, which is the second, quieter benefit.

One gotcha from setting this up: agent definitions register when a session starts. I created the files, dispatched work to them in the same session, and got “agent type not found.” Restart, and they were there.

The decision, as a flowchart
#

flowchart TD
    Q1{"Must it happen
every single time?"} -- yes --> H["Hook
(deterministic script)"] Q1 -- no --> Q2{"Is it needed in
every session?"} Q2 -- yes --> C["CLAUDE.md
(always in context)"] Q2 -- no --> Q3{"Is it knowledge the model
should load on demand?"} Q3 -- yes --> S["Skill
(loaded when relevant)"] Q3 -- no --> Q4{"Does it need isolated context,
different tools, or a cheaper model?"} Q4 -- yes --> A["Subagent
(own context and model)"] Q4 -- no --> P["Just put it in the prompt"]

Worked examples from my own config, one per branch: claude-name-session had to happen every time, so it is a hook. “Use yadm, not git” applies to every dotfiles session, so it lives in that repo’s CLAUDE.md (memory docs). Blog conventions load on demand, so they are a skill. Checklist verification wants a cheap model and a disposable context, so it is a subagent. And a one-off “rename this variable” needs none of the above.

Lessons
#

  • The layer question is “who decides this runs,” not “where does config go.” Hooks decide with code, skills and delegation decide with model judgment, CLAUDE.md never decides at all.
  • If one missed execution would annoy you, it is a hook. Writing MUST in a skill description is the smell that you picked the wrong layer.
  • Skills are for knowledge that is expensive to carry and cheap to load. If it applies to every session, promote it to CLAUDE.md; if it applies to one task family, keep it a skill.
  • Subagents are a pricing feature as much as a context feature. Route mechanical work to cheap models and keep the expensive context for judgment.
  • Agent definitions load at session start. Create, restart, then dispatch.

References
#

Taming Claude Code Sessions - This article is part of a series.
Part 6: This Article

Related

Your First Claude Code Hook: Auto-Name Every Session

··888 words·5 mins
In Part 1 we learned that named sessions are easy to find. Now let's make naming automatic with a hook. This is also a perfect first hook project, so I'll explain the whole idea from scratch. Taming Claude Code Sessions 路 Part 2 of 6 1 2 3 4 5 6 馃И Tested with Claude Code 2.1.x 路 macOS / Linux Part 1 left us with one chore: you still have to remember to name your sessions. Let’s delete that chore.

Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call

··2355 words·12 mins
Claude Code wants to run a shell command. I want to press a physical Stream Deck key, the YES key two inches to the left of my keyboard, to approve it. The hook gets exactly one HTTP response to decide allow vs deny. The key press might land in 200 milliseconds; it might land seven minutes later, after I've been pulled into a meeting and come back. The trick is that Claude Code's hook timeout is 600 seconds, which turns out to be just enough headroom to hold the HTTP response open the whole time and let a hardware button write the answer. Building ClaudeDeck 路 Part 6 of 10 1 2 3 4 5 6 7 8 9 10 馃И Tested with Claude Code 2.1.x 路 macOS (Setup, for anyone who hasn’t seen this stack before: Claude Code is Anthropic’s terminal CLI for Claude, and one of its hook events, PreToolUse, is a script Claude spawns and waits on before running a tool like Bash or Edit. The script’s stdout decides “allow” / “deny” / “ask”. Stream Deck is Elgato’s USB grid of programmable LCD keys. The plumbing I’m describing here lives in a daemon, a background process at 127.0.0.1:9127, that the hook script POSTs to and that the Stream Deck plugin connects to over WebSocket. For the hooks docs themselves and the four other gotchas in that layer, see the hooks-reality post.)

The Claude Code hooks docs are wrong. Here's what's actually on the wire.

··1709 words·9 mins
I wrote a daemon to listen to Claude Code hooks. My first version read `$CLAUDE_HOOK_PAYLOAD` and logged empty bodies for two days straight. The payload was sitting on stdin the whole time. Building ClaudeDeck 路 Part 1 of 10 1 2 3 4 5 6 7 8 9 10 馃И Tested with Claude Code 2.1.x 路 macOS This post is the five gotchas I hit while wiring up ClaudeDeck, a Stream Deck plugin (a small program that runs inside Elgato’s Stream Deck app on the USB grid of programmable LCD keys) that talks to Claude Code over its hooks system. Claude Code is Anthropic’s terminal CLI for Claude (claude in your shell), and its hooks are user-defined scripts it spawns at certain points in a session (before a tool call, on session start, on prompt submit). My daemon is a long-running background process the plugin and the hooks both talk to over a local socket. None of the gotchas are exotic. All of them cost me hours. Each one is a place where the docs were either silent, ambiguous, or contradicted by tribal knowledge I picked up from other people’s projects.