Skip to content

Build1 publisher3 min readPublished

Wiring "always lint" into a Claude Code Skill let the model skip it

A Hook fires on a lifecycle event whether the model agrees or not, while a Skill is pulled in by a per-turn judgement on its description. The exit code that enforces a Hook can also deadlock it.

The Engineer · Build desk

What happened

  • A developer writing on dev.to built an "always run the linter" step as a Claude Code Skill, and the agent skipped it on the turns when it judged the task did not need linting.
  • Claude Code now ships four ways to extend the agent, Hooks, Skills, Subagents and the Agent SDK. They overlap enough to look like alternatives and differ enough that misuse costs reliability, tokens, or both.
  • The 2026 hook event list adds SessionEnd, SubagentStart, SubagentStop, PreCompact and PostCompact to the original PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop and Notification set.
  • Skill loading has three tiers: name and description always in context at a few dozen tokens each, the body on invoke, and bundled files only when Claude reaches for them.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint A probabilistic router stays probabilistic however the description is edited, so a team that needs a step to run every time has to move it out of the Skill layer entirely.
  • cost Description length is a recurring bill: the text of every installed Skill is paid for on every turn of every session, and the same verbosity degrades routing accuracy.
  • exposure A blocking Hook runs with the operator's shell privileges and can refuse the agent's exit, so the deadlock stays invisible until someone exercises the exit-2 path deliberately.
  • decision The Hook-or-Skill call belongs in review when the requirement is written, because it settles whether the model gets a vote on the step at all.

The routing contract is short. Claude reads the `description` of every installed Skill at session start, decides per turn whether your prompt warrants pulling one in, and lets you force the matter with `/name` [20]. A description goes in, a judgement comes out. A Hook works the other way round: it is a shell command bound to a lifecycle event in settings.json, and the event alone decides whether it runs [10].

The linter the author actually wanted was a `PostToolUse` entry with the matcher `Write|Edit`, running `prettier --write "$CLAUDE_FILE_PATHS"` [11].

Hooks run with your shell privileges and outside the context window, so they are nearly free in tokens [19]. Skills bill differently. The first tier of progressive disclosure keeps a Skill's name and description in context permanently, a few dozen tokens each, which is how one agent can know about hundreds of them [21]. Take a few dozen as 40 and install 200 Skills: 8,000 tokens are resident before the user types anything [27]. That figure holds only while the descriptions stay one line. Write a paragraph in there instead and, according to the account, you have levied a permanent tax on the session and made the routing decision harder, so the model misfires more often [24].

Enforcement comes from the exit code, and one code does two jobs. Exit 0 is success. Exit 2 is a blocking error: stderr goes back to Claude and the action is rejected [13]. On a `PreToolUse` hook, that is how you stop the agent touching `.env` [14]. On `Stop` or `SubagentStop`, the same code means the agent is not allowed to stop and should keep going [15]. The author wired a `Stop` hook that checked for uncommitted changes and exited 2 when it found any; the agent refused to stop, tried again, still had uncommitted changes, and refused again [16]. "I had built a loop with my own hands," the author wrote [17].

"The lesson: a Hook that can block is a Hook that can deadlock. Test the exit-2 path before you trust it," the author wrote [18]. Tightening the Skill description was the wrong repair. The fault lay elsewhere. "The router did its job: it routed. Sometimes away from me," the author wrote [28].

The other two mechanisms relocate the decision. A Subagent is a Markdown file in `.claude/agents/` with frontmatter [26], running in its own context window and handing back a summary [7]. The Agent SDK is the same agent loop running in your own process [8]. The author's compressed version: "If you remember nothing else: Hooks remove a decision from the model, Skills add one, Subagents quarantine one" [9].

Skills are the right choice where the model genuinely should pick: a code review workflow, a procedure that drafts release notes from the git log, a triage routine, with the description written as a routing key [25].

This is one practitioner's account on dev.to, and it describes the linter Skill as working most of the time, with no miss rate attached [1]. The parts that transfer are the documented ones. Exit code semantics [13], the widened 2026 event surface with `SessionEnd`, `SubagentStart`/`SubagentStop` and `PreCompact`/`PostCompact` [12], and the three loading tiers, where Anthropic's own skill-development guidance keeps a SKILL.md body under about 500 lines [22].

What to watch

  • Whether the hooks reference documents the Stop and SubagentStop exit-2 loop as a hazard, or leaves it to be found in testing.
  • Whether Anthropic's roughly 500-line guidance for a SKILL.md body moves as installed Skill libraries grow.
  • A measured comparison of Skill routing hit rate against description length, which would test the claim that verbose descriptions misfire more.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories