Rules, Skills and Hooks: Which One for Which Job
Rules are a request, hooks are a guarantee, skills are the procedure in between. A decision table and three worked examples for choosing.
You wrote "always use pnpm, never npm" in your rules file in March. Last Tuesday the agent ran npm install, produced a package-lock.json, and you found it in the diff two commits later. The rule was there. It was clear. It was also, structurally, a suggestion.
Most agent setups have three mechanisms available and use one of them for everything. Sorting out which job belongs to which is the single cheapest improvement available, because the mechanisms have genuinely different guarantees and picking wrong is what produces the frustrating failures.
The three, and the one-line test for each
Rules are persistent instructions in a context file. The test: does this need to be true on every turn, and is it a fact or a preference rather than a procedure? The package manager. The test command. The fact that dates are stored as UTC timestamps and the codebase uses Result types rather than throwing. Things the model needs to know to not be wrong by default.
Skills are loadable procedures the model pulls in when the situation matches. The test: is this a multi-step thing you do occasionally and want done identically each time? Cutting a release. Adding an API endpoint with its route, handler, validation schema, client method and test. Onboarding a new package into the workspace. The knowledge is long, the trigger is specific, and it would be dead weight in a rules file on every unrelated turn.
Hooks are deterministic code your harness runs on an event. The test: must this happen every single time, regardless of whether the model remembers? Formatting on write. Blocking commits to main. Running the type checker after an edit.
Rules are a request, hooks are a guarantee
That is the whole thing, and everything else follows from it.
A rule is read by a model that is also holding your task, the file it is editing, four search results and its own half-formed plan. Adherence is high. It is not one. It degrades as the context fills, as the conversation gets long, and as the rules file gets longer, which is the part people find counterintuitive.
A hook is a shell command. It runs. It does not get distracted at turn forty.
So the question to ask about any instruction is not "is this important" but "what does it cost me when it is ignored once in thirty turns". If the answer is that you notice and fix it, a rule is fine. If the answer is a rewritten lockfile, a force-push to main, or a secret in a commit, it was never a rule. It was a hook you had not written yet.
The decision table
| You want | Frequency | Cost if forgotten | Use |
|---|---|---|---|
| Package manager is pnpm | Every turn | Broken lockfile, wasted diff | Hook (block npm i), plus a rule |
| Code is formatted | Every write | Noisy diffs, review friction | Hook |
No direct commits to main | Every commit | Bad, sometimes very bad | Hook |
| Types check after an edit | Every edit | Agent proceeds on broken code | Hook |
Prefer Result over throwing | Every turn | One inconsistent function | Rule |
| Tests live next to source | Every turn | A misplaced file | Rule |
| Cut a release | Monthly | Wrong tag, missed changelog | Skill |
| Add an API endpoint end to end | Weekly | Missing wiring, no test | Skill |
| Add a package to the workspace | Rarely | Broken CI config | Skill |
The middle column matters as much as the right one. A high-cost mistake you make once a quarter is a skill with a checklist. A low-cost mistake you would make forty times a day is a hook, because forty times a day is where "usually remembers" becomes "fails daily".
A rule that earns its place
## Conventions
- pnpm only. `pnpm --filter <pkg> test` to run tests.
- Money is stored as integer minor units. Never floats.
- API handlers return `Result<T, ApiError>`. Do not throw across the boundary.
- Tests sit beside source as `*.test.ts`, not in a `__tests__` directory.
- Dates are UTC on the wire, formatted at render time only.
Five lines, all facts, all load-bearing on an ordinary turn. Nothing about how to cut a release, nothing about the deploy pipeline, nothing about the history of the auth refactor.
A skill that earns its place
A release skill lives in its own file with a description precise enough for the model to know when it applies, and a body that is a checklist rather than an essay.
---
name: cut-release
description: Cut and publish a release. Use when asked to release, ship a version, or tag a build.
---
1. Confirm `main` is clean and CI is green: `gh run list --branch main --limit 1`
2. `pnpm changeset version` — do not hand-edit version numbers.
3. Review the generated `CHANGELOG.md`. Remove entries for internal-only packages.
4. `pnpm --filter "./packages/*" build && pnpm test`
5. Commit as `chore: release`, tag with the root version, push tag.
6. `pnpm publish -r --access public`
7. Post the changelog section to #releases.
Seven steps you would otherwise half-remember. It costs nothing on the 95 percent of turns that are not a release, because it is not loaded on those turns.
A hook that earns its place
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "pnpm exec prettier --write \"$CLAUDE_FILE_PATHS\" >/dev/null 2>&1"
}
]
}
]
}
}
Formatting is the ideal hook. It is fast, it is deterministic, it has no opinions to negotiate with, and it silently removes an entire category of diff noise. Blocking is the other ideal case: a PreToolUse hook matching Bash that inspects the command, exits non-zero on git push --force against main, and lets everything else through.
The 600-line rules file
The common failure is additive. Something goes wrong, you add a line. Six months later the file is 600 lines, it covers deployment, three deprecated patterns, a paragraph about a migration finished last year, and the model follows almost none of it.
Two things are happening. The instructions that matter are diluted by ones that do not, and the file is now large enough that a meaningful share of the context budget is spent restating things irrelevant to the current task. Adding a line to a long rules file measurably reduces adherence to the lines already in it. That is the trade you are making, whether or not you priced it.
Hold the file to roughly a page. When something does not fit, it is usually a procedure that belongs in a skill, or a guarantee that belongs in a hook. Very little genuinely belongs in a long rules file.
Hooks that are too slow or too chatty
The opposite failure is real too. A PostToolUse hook running tsc -b across the whole repo sounds responsible. Assume it takes 40 seconds and the session makes 25 edits: that is 1,000 seconds, sixteen minutes and forty seconds of pure waiting inside one session, and you will disable it by Thursday. Scope it to the touched package, or move it to a stop hook that runs once at the end.
Chattiness is subtler and worse. A hook that prints 300 lines of eslint output after every edit is injecting several thousand tokens of mostly-passing results into the context, pushing out the code the agent actually needs. Sensible default: a hook should finish in a second or two and print nothing when it succeeds. Output is for failures.
Skill or slash command
Same content, different trigger. A slash command fires when you type it, so use one when you know the moment and want it to fire only then. A skill fires when the model judges it relevant, so use one when the trigger is a situation rather than a decision, like "someone is adding an endpoint" or "someone is touching the migrations directory". If you would not reliably remember to type it, make it a skill and write the description carefully, because the description is the whole trigger.
Test it by trying to break it
After writing a rule, deliberately ask for the thing it forbids. Ask the agent to add a dependency with npm. If it does it, the rule is decorative and you have learned that in thirty seconds rather than in a diff review. Do the same with hooks: try to commit to main and confirm you are stopped. Re-run these checks after every meaningful edit to the rules file, because the failure mode is not that your new line does not work, it is that your new line quietly weakened an old one.
There is a useful discomfort in doing this honestly. The list of things still in your rules file after a proper audit is the list of things you have decided you can live with being ignored occasionally. Everything you cannot live with should already be code.
Keep reading
Building a Reusable Agent Setup You Actually Carry Between Projects
Stop re-teaching the agent your commit format on every new repo. Build a personal layer that travels with you, kept clean of the client's project layer.
The Tools Worth Paying For, and the Ones That Are Not
A category-by-category verdict on the subscriptions in your stack, plus the cancel test and a worked monthly budget for a solo developer.