Claude Code hooks vs Skills: which one actually costs more tokens?
12 min read

On this page
A hook fires because you told Claude Code exactly when to: a matcher on a tool call or session event, no exceptions, no model judgment involved. A Skill fires because Claude decided your prompt matched its always-resident description, or because you typed its name yourself - which means the exact same instruction, built as a Skill, can simply not run on the one session a hook would have caught every time. That fork happens before either mechanism's token cost enters the picture at all, and it's usually the more expensive option that ends up in your .claude/ directory, because it's the one that can't be skipped.
/name (113 tokens). Built as the same guardrail this article actually wrote and measured, a hook's deny reason costs 82 tokens on every matched call - the skill version's idle description plus invoked body lands near that too, but only on the sessions it actually fires. The two costs cross at roughly 26% invocation rate: below that, a skill's idle-then-load design is cheaper; above it, a hook's flat per-session cost wins.Who decides whether it runs: you, or Claude
A hook binds to an event, not to a conversation. Claude Code's hooks guide opens with the distinction directly: "Hooks are user-defined shell commands. Claude Code runs them at specific points in its lifecycle, which gives you deterministic control: certain actions always happen rather than relying on the LLM to choose to run them." The current hooks reference lists more than two dozen lifecycle events a hook can bind to - PreToolUse before a tool call executes, PostToolUse after one succeeds, SessionStart when a session begins or resumes, UserPromptSubmit when you submit a prompt, PreCompact before context compaction, and over twenty more down to niche ones like CwdChanged and WorktreeCreate. Skip the matcher and a hook fires on every occurrence of its event, full stop.
A Skill binds to your prompt, read against a resident description. Claude Code's skills guide is equally direct in the other direction: "Claude uses skills when relevant, or you can invoke one directly with /skill-name." By default the description field "helps Claude decide when to load the skill automatically" - it sits in context for the life of the session so Claude can judge relevance, but that judgment is exactly that: a judgment, not a guarantee.
- Event fires (PreToolUse, SessionStart, ...)
- Matcher checks the event, not your intent
- Hook script runs - every time it matches
Deterministic - “certain actions always happen rather than relying on the LLM to choose to run them,” per Claude Code's hooks guide.
- Description sits in context (default: on)
- Claude judges: does this look relevant?
- Skill body loads - only if Claude agrees, or you type /name
Model-decided - Claude “uses skills when relevant, or you can invoke one directly,” per Claude Code's skills guide.
Two frontmatter fields decide who's allowed to make that call - and they change what loads into context, not just who can invoke:
| Frontmatter | You can invoke | Claude can invoke | When loaded into context |
| --- | --- | --- | --- |
| (default) | Yes | Yes | Description always in context, full body loads when invoked |
| disable-model-invocation: true | Yes | No | Description not in context, full body loads when you invoke |
| user-invocable: false | No | Yes | Description always in context, full body loads when invoked |
That middle row is worth pausing on: a manual-only skill (disable-model-invocation: true, the pattern behind commands like /deploy or /commit) doesn't just stop Claude from triggering it - it drops the idle description out of context entirely, since there's no automatic-invocation decision for Claude to make. A skill you only ever run yourself costs nothing at all until the session where you actually type its name.
What each one costs when it actually fires
The per-fire numbers aren't new - this site measured them separately, for each mechanism on its own, before this guide put them side by side. The hooks cost breakdown found a one-time SessionStart context block running about 66 tokens, a terse PreToolUse deny reason at 38 tokens per matched call (which adds up fast under a broad matcher), and the gap between a filtered PostToolUse output (28 tokens) and an unfiltered one (447 tokens) - all estimated from realistic payloads built off the documented schema, not pulled from a live transcript. The subagents-vs-skills breakdown measured a Skill's idle description at 37 tokens and its full body at 113, run against real example text from Anthropic's own current docs.
Put next to each other, the shape is: a hook's cost is entirely about cadence - how often the event fires and how broad the matcher is. A Skill's cost is entirely about whether it loads at all - the idle tier is close to free, and the only expensive part is conditional on Claude's own judgment or your own typing.
The same guardrail, built two ways
Numbers from a hooks doc and numbers from a skills doc are still two different examples measured on two different days. So this run wrote one guardrail - "confirm before running a destructive bash command" - as both a hook and a skill, and ran both through this project's own token estimator (estimateTokens, packages/optimizer-cli/src/tokens.mjs, the same chars/4 heuristic behind every number on this site):
import { estimateTokens } from "./packages/optimizer-cli/src/tokens.mjs";
const hookDenyOutput = JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason:
"Blocked: rm -rf violates project policy - no destructive bash without review. Ask before running anything that deletes more than one file, or run git clean -n first to preview what would go.",
},
}, null, 2);
const skillDescription =
"Use when the user asks to delete files, clean up a directory, or run an rm command. Reminds Claude this project requires explicit user confirmation before any destructive bash command, and to preview with git clean -n or ls first.";
const skillBody = `Before running any destructive command (rm, rm -rf, git clean -f, truncate, or similar):
1. Preview what would be affected first - ls the target, or git clean -n / git status for anything git-tracked.
2. State exactly what will be deleted and ask the user to confirm, even if the request sounded confident.
3. Never chain a destructive command with && after another command without a separate confirmation step.
4. If the target includes a path outside the project directory, refuse and explain why.
This applies regardless of how the request is phrased - "clean this up", "get rid of the old files", and "rm -rf" all count.`;
console.log("hook deny output:", estimateTokens(hookDenyOutput), "tokens");
console.log("skill description:", estimateTokens(skillDescription), "tokens");
console.log("skill body:", estimateTokens(skillBody), "tokens");
$ node guardrail-tokens.mjs
hook deny output: 82 tokens
skill description: 58 tokens
skill body: 157 tokens
"Confirm before a destructive bash command" - this project's own tokenizer, run against text written for this article
The hook's 82 tokens is the more expensive single line - and it's also the only one of the three guaranteed to show up before the command runs. The skill's 157-tokenbody is cheaper to skip entirely, which is exactly the problem for a guardrail: cheap-to-skip and guardrail don't belong in the same sentence.
The numbers aren't really the point here - the trigger is. A PreToolUse hook binds to the Bash tool call itself, so it inspects every single one, including a destructive command Claude generates mid-turn that never resembled anything the user typed. A skill's description only has the user's prompt to match against; if nothing in the conversation looked like "delete" or "clean up" before Claude decided to run rm -rf on its own, the skill's description was never a candidate for invocation in the first place. That's why the hooks guide frames deterministic hooks as the tool for enforcing project rules, and reserves judgment calls for a specific, named exception: "prompt-based hooks" and "agent-based hooks" ("type": "prompt" and "type": "agent" in the hook config) that use a Claude model to evaluate a condition - hooks that still bind to the deterministic event, but decide their verdict the way a skill decides relevance. A guardrail that has to fire every time isn't a cost question at all; it's a "can this mechanism see the event" question, and only one of the two can.
Where the cost math actually flips the decision
Not everything is a guardrail. Plenty of what goes into a hook or a skill is standing information Claude benefits from having, but nothing breaks if it's missing on one session out of twenty - a note on your commit convention, a reminder of which directory holds generated files, a summary of how a legacy module works. For that class of content, the trigger-determinism argument above doesn't apply, and the decision really does come down to tokens.
Per-session token cost, by how often the skill would actually be invoked
Below ~26% invocation, the skill's idle-then-load design is cheaper - most sessions never pay the 113-token body. Above it, a hook that injects the same information unconditionally, every session, ends up cheaper than repeatedly paying to fetch it on demand. Two different measured examples, not the same content measured twice - the mechanism this shows generalizes even though your own numbers will differ.
The math: a hook that injects the note unconditionally costs its flat per-session price every time, regardless of whether that session needed it. A skill costs its idle description every session plus its full body only on the sessions Claude actually invokes it - so its average cost per session is idle + body x invocation_rate. Using this site's own two previously-measured examples (66-token hook inject, 37-token idle description, 113-token body - not the same content measured twice, so treat the exact crossover as illustrative), the two lines cross at roughly a 26% invocation rate. Below that, paying the skill's small idle tax and loading the body only sometimes wins. Above it, you're now paying to re-fetch something on demand more often than you'd have just paid to always have it - and the hook's flat cost wins instead.
A decision table for your next one
| What you're building | Reach for | Why |
| --- | --- | --- |
| A rule that must never be skipped (block a command, redact a secret, format on save) | A hook | Bound to the event itself; Claude can't decide not to trigger it |
| A rule that needs judgment, not a fixed pattern, but still must never be skipped | A prompt- or agent-based hook | Still bound to the deterministic event; the verdict uses a model, the firing doesn't |
| Knowledge or a procedure you want available but rarely need | A skill (default) | Idle tier costs a few dozen tokens; the expensive part only loads when actually used |
| Something you trigger yourself, on your own schedule, that shouldn't run automatically | A skill with disable-model-invocation: true | No idle description cost at all - nothing loads until you type /name |
| Standing information you want present in most sessions regardless of relevance | A hook, once invocation would clear roughly a quarter of sessions | Past that point, unconditional beats on-demand on cost alone |
Where they stack instead of compete
They're not mutually exclusive on the same job. A SessionStart hook can point Claude at a skill by name instead of inlining the whole procedure - a cheap, deterministic nudge ("see the deploy-checklist skill before touching production") that costs a fraction of a full body, while the skill itself stays out of context until actually needed. Going the other direction, a manual-only skill like /deploy pairs naturally with a PreToolUse guardrail hook around the same destructive commands the skill's own steps might run - the skill carries the procedure, the hook carries the "you can't skip this part" enforcement, and neither one substitutes for the other's job.
Hooks can also call an MCP tool directly ("type": "mcp_tool") or POST to an HTTP endpoint ("type": "http") instead of running a local shell command - which matters for cost because those payloads follow the same five-field rule as command hooks: the request body itself never reaches Claude, only whatever the handler writes back into additionalContext, systemMessage, or a deny reason does.
When neither mechanism is the answer
If the actual problem is a large exploration or side-task that would otherwise flood your main context - not a rule to enforce or knowledge to keep handy - that's a subagent's job, not a hook's or a skill's, and it has its own separate cost profile worth reading before reaching for either mechanism here. And if the standing cost you're trying to cut isn't something firing per-turn at all but something registered and idle for the whole session - an MCP server you configured months ago and forgot about - hooks and skills won't touch that; disabling the server itself is the actual lever.
FAQ
- Do Claude Code hooks or Skills cost more tokens? Neither one, in general - it depends on cadence for hooks and invocation rate for skills. A hook's cost is fixed per matched event and adds up with frequency; a skill's cost is nearly free until Claude (or you) actually invokes it, then it's a one-time load per session.
- Can a hook use Claude's own judgment the way a skill does? Yes, with
"type": "prompt"or"type": "agent"hooks - both use a Claude model to evaluate a condition, unlike the default"type": "command"hook. They still bind to the deterministic event; only the verdict is judgment-based, not the firing. - Does a skill's description really cost tokens even if I never use it? Yes, by default - it stays resident in context for as long as the skill is installed and enabled, whether or not it's ever invoked. Setting
disable-model-invocation: trueremoves the description from context entirely, at the cost of Claude never being able to trigger it automatically. - What's the cheapest way to give Claude a standing instruction I use constantly? If "constantly" means most sessions, a hook that injects it unconditionally is usually cheaper than a skill re-loaded on demand past a certain invocation rate - see the crossover math above. If "constantly" means rarely but predictably, a skill's idle tier is the cheaper design.
- Can hooks and skills work on the same problem together? Yes - a hook can point Claude at a skill by name instead of inlining a full procedure, and a manual-only skill pairs naturally with a hook that enforces the part of the same workflow that can't be skipped.
- Do these measured numbers match what my own setup pays? Not exactly - they come from example payloads and Anthropic's own documentation samples, not your project's actual hooks and skills. The mechanism (hooks are cadence-priced, skills are invocation-priced) holds regardless; your own token counts depend on how verbose your specific hooks and skill bodies are.
Reading your own hooks.json and skills to see which side of that 26% crossover you're actually on is exactly what the hook token cost calculator does with your real config instead of these worked examples. And if hooks and skills turn out to be the smaller piece of your setup, the free scan reads your real Claude Code session history and tells you where the tokens are actually going before you spend more time auditing either one by hand.
See your own numbers
These are aggregates from real sessions. Your setup is different - run the free scan and get the breakdown for your own Claude Code history. It runs locally; nothing about your code or prompts leaves your machine.
npx usagecutRun a free scan →UsageCut by ClockedCode - not affiliated with Anthropic. The figures on this page are measured on real Claude Code sessions and labeled measured or estimated where it matters.