Claude Code Hooks and Skills: Automating Your Dev Workflow
Most developers use Claude Code the same way they'd use any chat interface: type a prompt, wait for a response, review the diff, repeat. That works fine for one-off tasks, but it leaves a lot of value on the table. Once you're running an agent inside your actual repository, with actual shell access, you can wire it into the same automation instincts you already apply to CI pipelines. Two features make this possible: hooks, which are shell commands that fire automatically on specific agent events, and skills, which are reusable, discoverable procedures the agent can invoke by name instead of you re-explaining a workflow every session. Together they turn Claude Code from a smart autocomplete into something closer to a scriptable teammate. This article walks through both, with working config examples you can drop into a project today.
What Hooks Actually Are
A hook is a command your shell already knows how to run, triggered by an event in the Claude Code lifecycle instead of by you pressing Enter. The event types are things like PreToolUse (before a tool call executes), PostToolUse (after it completes), Notification (when the agent wants your attention), and Stop (when a session or subagent finishes). You register hooks in a settings file, and the harness — not the model — executes them. That distinction matters: a hook is not a suggestion you're hoping the model remembers to follow. It is a shell command guaranteed to run when its matcher fires, regardless of what the model "wants" to do.
This is the core reason hooks exist. Model instructions in a system prompt or a CLAUDE.md file are strong nudges, but they are still probabilistic. If you write "always run prettier after editing a file" in your project instructions, the agent will usually do it, and occasionally forget, especially in long sessions where context gets crowded. A PostToolUse hook matched on Edit|Write that runs prettier --write on the touched file removes the "usually" — it becomes deterministic. The same logic applies to guardrails: telling the model "never run rm -rf" is advice; a PreToolUse hook that inspects the command and exits non-zero to block it is enforcement.
The practical mental model: hooks are for anything you'd be uncomfortable leaving to chance. Formatting, linting, running a test suite before a commit lands, blocking destructive commands, logging tool usage for audit trails, pinging Slack when a long task finishes — all deterministic, all better as hooks than as prose in a prompt.
Common Hook Use Cases
Lint-on-save. Every time the agent edits or writes a file, run the project's linter or formatter against just that file. This keeps the codebase consistent without you having to remind the agent, and it catches issues before they compound across a multi-file change.
Test-on-commit. Before a git commit command is allowed to execute, run the relevant test suite (or at minimum a fast subset) and block the commit if it fails. This is the same discipline as a pre-commit hook in Husky or a native git hook, except it's enforced at the agent layer, so it applies even when the agent is the one drafting the commit.
Guardrails against destructive commands. Pattern-match on the command string in a PreToolUse hook for Bash calls. Anything matching rm -rf, git push --force to a protected branch, DROP TABLE, or credential file access gets blocked or requires explicit confirmation. This is cheap insurance against a model misreading an instruction or hallucinating a destructive step under pressure.
Type-checking after edits. For TypeScript or typed Python codebases, run tsc --noEmit or mypy after a batch of edits so type errors surface immediately instead of at the end of a long session.
Secrets and diff scanning. Before allowing a git add or git commit, scan the staged diff for patterns that look like API keys, tokens, or .env contents, and block the commit if something looks like a leaked secret.
Session notifications. On Stop or Notification events, fire a curl to a Slack webhook or a local terminal-notifier call so you know when a long-running agent task has actually finished, instead of babysitting a terminal tab.
Audit logging. Append every Bash command the agent runs to a local log file. Useful for compliance-conscious teams, or just for your own postmortems when something went sideways.
Notice the pattern across all of these: hooks work best when they're narrow, fast, and deterministic. A hook that takes 90 seconds to run on every single file edit will make the agent feel sluggish. A hook that occasionally throws an unrelated error will erode trust fast, because the agent (and you) will start ignoring its output. Keep hooks scoped, keep them fast, and make their exit codes meaningful.
Anatomy of a Hook Config
Hooks live in a settings file — project-level .claude/settings.json for team-shared hooks, or a user-level settings file for personal ones. Structurally, a hook config is a mapping from an event name to a list of matchers, and each matcher maps a tool-name pattern to one or more commands to run.
Conceptually, three things matter for every hook you write:
- The event — when does this fire?
PreToolUseruns before the tool call, so it can inspect and block.PostToolUseruns after, so it's for cleanup, formatting, or verification. - The matcher — which tool calls does this apply to? You can match on tool name (
Bash,Edit,Write) and often on the specific command pattern within that tool call. - The command — what shell command actually runs? It receives the tool call's input (like the file path or command string) via environment variables or stdin, and its exit code determines whether the harness treats it as a pass, a warning, or a hard block.
Here's a concrete example — a PostToolUse hook that auto-formats JavaScript and TypeScript files after every edit, plus a PreToolUse guardrail that blocks force-pushes to main:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$CLAUDE_TOOL_INPUT\" | grep -E 'push.*--force.*main' && exit 2 || exit 0"
}
]
}
]
}
}The formatting hook is deliberately forgiving — it swallows errors with || true so a Prettier config issue doesn't halt an otherwise-fine edit. The force-push guard is deliberately strict — exit code 2 is treated as a hard block, so the agent sees the rejection and has to explain itself or choose a different path. That asymmetry is intentional: be lenient on cosmetic hooks, be strict on safety hooks.
A test-on-commit variant follows the same shape, just matching on the commit command itself and running your suite before allowing it through:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'if echo \"$CLAUDE_TOOL_INPUT\" | grep -q \"git commit\"; then npm test --silent; fi'"
}
]
}
]
}
}If npm test fails, the non-zero exit propagates and the commit is blocked. The agent sees the failing test output and has to fix it before trying again — the same discipline you'd want from a human contributor, just automated.
What Skills Are and Why They're Different from Hooks
If hooks are about *enforcement*, skills are about *packaging expertise*. A skill is a named, discoverable procedure — usually a markdown file with a description and a set of instructions — that the agent can recognize as relevant to the current task and invoke, either automatically or when you explicitly ask for it by name.
The problem skills solve is repetition. Say your team has a specific way of debugging flaky CI failures: check the logs for a known set of error signatures, re-run with verbose tracing, compare against the last five passing runs, and only then start changing code. You could re-explain that procedure in every session. Or you could write it once as a skill, give it a clear trigger description, and let the agent pull it in whenever the situation matches — "debug this failing test," "why did CI fail," "investigate this flaky build" — without you having to remember the exact phrasing that makes it fire.
Skills are discoverable by design. The agent sees a short description of every available skill and decides, based on the current task, whether one applies — much like it decides which tool to call. This is the key difference from a hook: a hook always fires when its matcher condition is true. A skill is a candidate that the agent chooses to invoke because the task looks like a match. That makes skills better suited for judgment-heavy, multi-step workflows and worse suited for anything that must happen unconditionally.
What Makes a Good Skill
A skill file that never gets invoked, or gets invoked at the wrong time, isn't useful no matter how good its internal instructions are. Triggering is the whole game. A few things separate skills that actually get used from ones that quietly rot in a .claude/skills/ folder:
- A sharp trigger description. The description is what the agent matches against, not the body. It needs to name the situations that should invoke it and, just as importantly, the situations that shouldn't. "Use this for debugging" is too vague and will either never fire or fire on everything. "Use this when a test that previously passed is now failing intermittently, especially in CI, to systematically narrow down the root cause before changing code" is specific enough to trigger reliably and to stay quiet when it shouldn't.
- Concrete, ordered steps. Skills work best as procedures, not philosophy. "Think carefully about the bug" is not a step. "Reproduce the failure locally with the same seed/flags as CI; if it doesn't reproduce, check for timing or ordering dependencies first" is a step. Numbered lists work well here because they force you to commit to an order, and order usually matters in debugging and review workflows.
- An explicit "when NOT to use this" section. This is the part people skip and shouldn't. Without it, a skill for "systematic debugging" might get invoked on a simple typo fix, adding five minutes of ceremony to a thirty-second task. Stating the boundary — e.g., "skip this for straightforward, reproducible bugs with an obvious cause; use it for intermittent, environment-dependent, or previously-unsolved failures" — keeps the skill scoped to where it earns its keep.
- Self-containment. A skill should work without assuming the agent remembers anything from earlier in the conversation. Include the context, the file paths or naming conventions it needs, and any team-specific vocabulary inline.
- A bounded scope. One skill, one job. A skill that tries to cover "debugging, code review, and deployment" is really three skills wearing a trenchcoat, and it will trigger unpredictably because its description has to be vague enough to cover all three.
Example Skill File Structure
Skills are typically a directory with a markdown file describing the procedure, sometimes alongside supporting scripts or templates. Here's the shape of a "systematic debugging" skill:
---
name: debug-systematically
description: >
Use when a bug is intermittent, hard to reproduce, environment-dependent,
or has already resisted a quick fix. Do NOT use for simple, obviously
reproducible bugs with a clear one-line cause — just fix those directly.
---
## Debug Systematically
1. Reproduce the failure locally first. If it won't reproduce outside CI,
suspect timing, ordering, environment variables, or shared state before
suspecting the code itself.
2. Read the full error output and stack trace before forming a hypothesis.
Do not guess at a cause from the error message's first line alone.
3. Check recent changes: `git log -p --since="7 days ago"` on the affected
files. Most regressions are recent.
4. Form one hypothesis at a time. Add a minimal, targeted log line or
assertion to confirm or rule it out. Do not change production logic to
"see if it helps."
5. Once confirmed, write the smallest fix that addresses the root cause,
not the symptom. If the fix is a workaround, say so explicitly and file
a follow-up.
6. Add or update a test that would have caught this before closing out.The frontmatter is doing the real work — the description field is what the agent scans to decide relevance, so it needs both the positive trigger and the negative one, in the same sentence if possible.
Useful Skills for a Dev Team
A few skills earn their place in almost any team's setup:
Debug systematically (shown above) — prevents the agent from thrashing on intermittent bugs by forcing a reproduce-first, one-hypothesis-at-a-time discipline instead of speculative fixes.
Code review checklist — a skill triggered by "review this PR" or "review my diff" that walks a consistent checklist: correctness, security-sensitive patterns (unvalidated input, secrets in code, unsafe deserialization), test coverage for the changed lines, and naming/consistency with the rest of the codebase. The value here isn't that the agent couldn't review code without it — it's that the checklist is *consistent* across every review, the same way a human reviewer with a rubric catches more than one relying purely on vibes.
Database migration safety — triggered when a schema change is proposed. Checks for backward compatibility (can the old code run against the new schema during a rolling deploy), whether an index is needed before a large backfill, and whether the migration is reversible.
Incident writeup — triggered after a production issue is resolved. Walks through timeline reconstruction, root cause, blast radius, and a follow-up action list, formatted the way your team's postmortems are always formatted, so nobody has to remember the template.
Dependency upgrade — triggered when bumping a major version of a library. Checks the changelog for breaking changes, greps the codebase for usages of any removed or changed APIs, and runs the test suite before and after.
Each of these is really just a codified version of "the way our best senior engineer already does this task." That's the honest way to think about skills: you're not teaching the model something it can't do, you're making sure it does the thing the same disciplined way every time, instead of whatever way seems reasonable in the moment.
Hooks and Skills Working Together
The two aren't competing mechanisms — they compose. A skill might describe a multi-step code review procedure, and one of its steps is "run the linter and the test suite," which is enforced underneath by hooks so it's not just a step the agent might skip when it's confident the code "looks fine." A debugging skill might instruct the agent to reproduce a failure locally, and a PreToolUse hook independently blocks it from committing until that reproduction step's test actually passes.
Think of hooks as the floor — the things that must always happen, no exceptions, enforced by the harness. Skills are the judgment layer on top — the playbooks that get selected and applied based on what the task actually looks like. A well-configured project has both: hooks catching the mechanical, always-true rules (format this, never run that, block this pattern), and skills capturing the procedural, context-dependent expertise (how we debug, how we review, how we migrate schemas).
Getting Started Without Overbuilding
The temptation, once you see what's possible, is to write twenty hooks and fifteen skills in one sitting. Resist it. Start with one hook that removes a genuine annoyance — auto-formatting on save is usually the easiest win, because it's low-risk and immediately visible. Add one guardrail hook around the command you're actually nervous about the agent running (force-pushes, rm -rf, prod database access). Then write one skill for the workflow you find yourself re-explaining most often — for most teams that's either code review or debugging.
Once those are in place and you trust them, expand. You'll notice the pattern quickly: every time you catch yourself typing the same instruction into a Claude Code session for the third time, that's a signal it belongs in a skill. Every time you think "I really hope it doesn't do X," that's a signal X belongs in a hook, not a hope.
This is also where a lot of teams get their automation backwards: they try to write the perfect hook or skill in one pass, covering every edge case up front. It's better to ship a narrow version, watch it run for a week of real sessions, and tighten the matcher or the trigger description based on what actually happened — false positives, missed triggers, hooks that were too slow. Treat your hooks and skills config the same way you'd treat any other piece of infrastructure: version it, review changes to it, and expect to iterate.
Closing Thoughts
Hooks and skills are the difference between using Claude Code as a chat window and using it as a system you've actually engineered around your team's standards. Hooks give you deterministic enforcement over the things that shouldn't be left to chance — formatting, testing, safety. Skills give you a way to encode the judgment-heavy procedures your best engineers already follow, so the agent applies them consistently instead of reinventing an approach every session. Neither requires exotic tooling — a hook is a shell command in a JSON matcher, a skill is a markdown file with a good description — but the leverage compounds fast once they're wired into your actual workflow.
If you want to go deeper — building multi-agent workflows, wiring hooks into real CI pipelines, designing skill libraries for a whole team, and shipping production AI features on top of Claude Code — that's exactly what we cover, hands-on, in Vibe Coding AI Apps with Claude Code, the course Ira Menon and I built for engineers who want to move past prompting and start engineering with these tools.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
Claude CodeGo from zero to confident with Claude Code, the terminal agent that reads, edits, runs, and verifies real code.
Related reading