Skills and Hooks: Claude Code Patterns Worth Copying
The skills directory structure (what you keep there, what you don't), the hooks config, and the three workflows we have automated this way across client projects.
Most teams that adopt Claude Code start with a CLAUDE.md file, add a few permission rules, and stop there. That works for a week. What it misses is the two features that take the harness from a smarter autocomplete to something that genuinely runs parts of your workflow for you, which is the whole promise of the tool. Those two features are skills and hooks. Skills are reusable workflows the agent can pull in on demand. Hooks are deterministic scripts the harness runs at fixed points in the loop, whether the agent asked for them or not. Together they cover a gap that CLAUDE.md alone cannot, and once you have them set up across a client repo the agent stops forgetting things, stops skipping steps, and stops needing to be reminded of the same constraint three times a session. This article covers how we structure the skills directory, what belongs in settings.json hooks, the distinction between a skill and a rule that almost no one gets right on the first try, and the three workflows WitsCode has automated this way on every client project where it fits.
Skills versus rules, because the distinction is load-bearing
A rule is a constraint. Do not commit secrets. Use tabs not spaces. Never push to main. Rules belong in CLAUDE.md or in permissions config, and the agent reads them once at session start and is expected to remember them for the duration. A rule is static, declarative, and short.
A skill is a workflow. Run the pre-deploy check, which means running the type checker, running the lint pass, opening the migration diff, and writing a short report before staging. A skill is a sequence. It has steps, it may read supporting files, and it usually only activates when the agent notices a trigger in the current task. A skill is dynamic, procedural, and as long as it needs to be.
Teams that conflate these two end up with a CLAUDE.md that has grown to nine hundred lines, full of procedures the agent is supposed to remember at all times but mostly does not, because procedures are too heavy to carry in working memory. The fix is to move the procedures into skills, leave only the constraints in CLAUDE.md, and let the harness load the skill on demand when the trigger fires. After that refactor a seven hundred line CLAUDE.md typically shrinks to under a hundred, and the agent follows the workflow more reliably because it reads the skill at the moment it is needed, with full detail, rather than trying to recall a section of a long instruction file.
The SKILL.md plus supporting files pattern
A skill in Claude Code lives in .claude/skills/<skill-name>/ and the entry point is a file named SKILL.md. The frontmatter of that file contains a name and a description, and the description is the only part the agent sees until it decides the skill is relevant. That means the description has to be written like a trigger. It should name the situation the skill applies to, not the action the skill performs. "Run the deploy check" is a bad description because the agent does not know when to invoke it. "Use this before any production deploy or when the user says 'ready to ship'" is a good description because it tells the harness when the skill should fire.
The rest of the SKILL.md is the workflow itself, written as prose with embedded commands. Keep it under two hundred lines. If the skill needs more detail, put it in supporting files in the same directory and reference them by relative path. The agent will read the supporting files only when it reaches the step that needs them, which keeps the context window small on skills that activate often but only use their full detail occasionally.
A minimal skill directory looks like this.
.claude/
skills/
deploy-check/
SKILL.md
checklist.md
rollback-template.md
supabase-migration-gate/
SKILL.md
migration-naming.md
rls-policy-patterns.md
tailwind-theme-sync/
SKILL.md
tokens.json
component-audit.md
What does not belong in the skills directory is anything the agent needs every session regardless of task. Codebase conventions, tech stack, deploy target, forbidden commands, those all belong in CLAUDE.md because they are rules not workflows. If you find yourself writing a skill called "general-guidance" or "always-do-this", it is a rule and it belongs one level up.
The hooks that actually matter
Hooks are configured in .claude/settings.json under a hooks key, and they run at fixed points in the agent loop regardless of what the agent is doing. There are several hook types available but in practice only a few earn their keep on a client repo, and the rest create more noise than signal.
The two hooks we configure on almost every project are PostToolUse for linting and SessionStart for context priming.
PostToolUse fires immediately after the agent calls a tool, and with a matcher on Edit|Write|MultiEdit it becomes the place to run the linter and formatter on whatever file was just changed. The value of running it as a hook rather than asking the agent to run it is that the hook runs every single time, whether the agent remembers or not, and the output lands in the agent's next turn as tool feedback. If the lint fails, the agent sees the failure and fixes it before moving on, without needing a rule in CLAUDE.md that it might or might not recall.
SessionStart fires once when the session begins, and it is where you run the commands that prime the agent with fresh context. Git status, current branch, the last three commits, the output of a quick env check. None of that belongs in CLAUDE.md because it changes between sessions, and none of it belongs in a skill because the agent should not have to go look for it. A small SessionStart hook that injects those four pieces of information at the top of the context window saves the agent twenty tool calls over the course of a typical session.
A representative hooks block looks like this.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "git status --short && echo '---' && git log --oneline -3"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npm run lint:staged --silent || true"
}
]
}
]
}
}
UserPromptSubmit and PreToolUse exist and are occasionally useful. UserPromptSubmit is where you would inject a reminder that is specific to one phase of a project, for example a banner that says "we are in code freeze until Friday" that you want stamped on every prompt. PreToolUse is where you would block a dangerous command before it runs, for example any rm -rf on a protected path. Both of these are useful when you need them and intrusive when you do not, so we add them per project rather than by default.
Workflow one: deploy-check
The first skill we install on every client project is deploy-check. The trigger in its description is any mention of deploy, ship, push to production, or release. The workflow has five steps, executed in order, and the skill's SKILL.md is structured as a checklist the agent must complete in full before reporting back.
The steps are: run the type checker and capture the output, run the test suite with the CI profile not the local profile, open the migration directory and report any pending migrations that are not yet applied on staging, run a bundle size check against the previous commit, and finally print a short go or no-go summary with the reasoning. The skill is not allowed to skip steps even if one of them passes trivially, because the value of the skill is that the agent runs the full sequence every time, which is exactly what humans get tired of doing by deploy number fifteen.
The reason this works better as a skill than a hook is that deploys are not tied to a tool call. They are tied to an intent expressed in natural language, and skills activate on intent while hooks activate on tool calls. A PostToolUse hook cannot know that the user is about to ship. A skill with a good description does.
Workflow two: supabase-migration-gate
The second skill is specific to projects on Supabase, and it exists because migrations are the single most common source of production incidents on small to mid-size teams. The trigger is any edit to a file under supabase/migrations/ or any mention of creating a new migration. The workflow enforces naming convention, checks for a corresponding rollback in the same migration, verifies that any new table has an RLS policy defined in the same file, and runs the migration against a local Supabase instance before allowing the agent to stage it.
The supporting files in this skill do real work. migration-naming.md has the timestamp format and the allowed verbs in the migration name. rls-policy-patterns.md has the five RLS patterns the client uses, with examples, so the agent writes policies that match the rest of the codebase rather than inventing a sixth pattern. Neither file is loaded until the skill activates, which is the pattern that keeps the context window clean.
We originally tried to enforce the migration gate as a PreToolUse hook that blocked any write to the migrations directory without the rollback clause present. It worked in the sense that it blocked the write, but the agent did not understand why, and would retry the same write in a slightly different form. Moving the logic to a skill where the agent reads the rules, writes the migration correctly the first time, and understands the reasoning produced better migrations and fewer retries. The lesson, which generalises, is that hooks are good at enforcement and bad at teaching, and skills are good at teaching and therefore rarely need enforcement.
Workflow three: tailwind-theme-sync
The third skill handles a problem specific to frontend work with a design system. When a designer updates a token in the Figma file, the tailwind config needs to be updated, the CSS variables need to be regenerated, any component that used the old token value needs to be audited, and the design system documentation page needs to reflect the new values. None of those steps are difficult. All of them get missed if a human does them by hand, and the result is a codebase where half the buttons use the new brand blue and half still use the old one.
The skill takes a tokens.json file as input, updates tailwind.config.js, regenerates the CSS variable block in src/styles/tokens.css, greps the codebase for literal colour values and flags any that look like they should have been tokenised, and updates the documentation. It runs end to end in under thirty seconds on a typical project, and the output is a report the designer can read and sign off on.
The hook that pairs with this skill is a PostToolUse that runs npm run build:tokens any time tokens.json changes. The skill does the thinking and the audit. The hook makes sure the build artifact never drifts from the source. Split that way, each piece does what it is good at and neither tries to do the other's job.
Where to keep all of this
Skills and hooks both live in .claude/ at the project root, committed to the repo. That is the answer to the most common question we get on this setup, which is whether skills should be user-scoped (in ~/.claude/) or project-scoped. User-scoped skills are fine for personal workflows that apply to everything you work on, but any skill that encodes how a specific codebase handles migrations, deploys, or tokens belongs in the repo where every contributor and every agent session gets the same version. Check them in, review changes to them in pull requests, and treat them as part of the codebase rather than personal config.
The same applies to settings.json hooks. Project-scoped hooks in the repo, committed, reviewed. The only things that belong in user-scoped settings are truly personal preferences, the model choice, the theme, the keybindings.
A codebase with a well-tended .claude/ directory is the closest thing there is to a developer onboarding experience that scales. A new contributor clones the repo, opens Claude Code, and the harness loads the same skills, the same hooks, and the same rules that every other contributor uses. That is worth the hour it takes to set up on day one of a project, and it compounds over every session after that.
If you want this set up on your codebase without spending the week it takes to tune the skill descriptions and the hook matchers until they fire at the right moments, WitsCode installs the pattern end to end, tailored to your stack and your deploy target. → Book a Claude Code setup engagement with WitsCode.
Get weekly field notes.
Practical writing on shipping products, straight to your inbox. No spam.
Need help with this?
MVP Development
We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.
Talk to usWant to discuss vibe coders for your business?
Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.