
You've been using the slash commands Claude Code ships with. Now write your own, and wire the hooks that fire on their own. Bundle a repeatable workflow into a command you trigger with a slash, then back it with hooks that enforce the rules a command can only ask for: build a /commit command, auto-lint after every edit, block writes to your migrations folder, and force the tests to pass before a turn can end.
Episode 3 covered the slash commands Claude Code ships with. This one is about writing your own, plus the automation that fires whether Claude remembers it or not. Two primitives, one rule for choosing between them: a custom slash command is a shortcut you trigger; a hook is a guarantee that runs on its own.
Custom slash commands. Where they live (a per-command folder under your project's .claude or your home .claude, with the folder name becoming the command), the SKILL.md file and its YAML frontmatter, and the fields that matter: description, argument-hint, allowed-tools, model, and disable-model-invocation. Passing arguments with $ARGUMENTS and positional $1/$2, injecting live shell output into the prompt with the !`...` prefix, and pre-approving tools so a workflow runs without permission prompts. Worked example: a /commit command that stages, writes a real message, pushes, and opens a PR. From the skills documentation.
Hooks. The lifecycle events (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart/End, compaction, notifications, subagent events), how they're configured under the hooks key in settings.json, the matcher syntax, the JSON a hook receives on stdin, and how it answers back through exit codes (0 success, 2 blocks) and a decision JSON that can allow, deny, ask, modify tool input, or add context. Three hooks worth stealing: auto-lint and format after every edit, block writes to the migrations folder, and a Stop hook that forces tests to pass before a turn can end. From the hooks documentation.
The pitfall. Putting an "always" or "never" rule inside a command's prose and trusting it. Command instructions are advice, like CLAUDE.md; if the cost of the model forgetting is real, that rule belongs in a hook. Plus quick traps: hook timeouts, unquoted arguments, and vague descriptions that never auto-invoke. Browse what you've built with /hooks, /skills, and /help.
In the last episode we walked through the built-in slash commands, the ones that ship with Claude Code: clear, compact, context, review, rewind, and the rest. Those get you fast. But every one of them was written by somebody at Anthropic for everybody. This episode is about the moment you stop using other people's shortcuts and start writing your own.
There are two ways to teach Claude Code a workflow, and the whole episode hangs on the difference between them. The first is the custom slash command. You bundle up a repeatable set of steps, give it a name, and from then on you type a slash and that name and the whole routine fires. It's a shortcut you pull when you want it. The second is the hook. A hook is a script that runs on its own, automatically, the moment some event happens inside a session. You don't invoke it. It's wired to a trigger and it fires whether Claude remembers it should or not.
Here's the mental model worth carrying through the next half hour. A slash command encodes the steps you do often enough to want a shortcut. A hook encodes the checks that have to run every single time, no exceptions, no matter what Claude happens to be doing. One is a convenience. The other is a guarantee. Most of the leverage you'll get from Claude Code over the next few episodes comes from knowing which of those two you actually need for a given problem, and reaching for the right one.
Let's build a custom slash command first, because it's the friendlier of the two and you'll use it within the hour.
Writing your first custom slash command
Inside your project there's a hidden folder, dot claude, the same one that's been holding your settings file and your project memory since the first episode. To make a custom command, you create a skills folder inside it, and then one folder per command. The name of that folder becomes the command you type. So a folder named fix-issue gives you a command you invoke by typing slash fix-issue. That's the whole naming rule. The directory name is the command name.
Inside that folder you put a single markdown file. Claude Code calls it SKILL, in all caps, with a markdown extension. That file has two parts. At the top, between two lines of three dashes, is a little block of settings called frontmatter, written in YAML. Below that is plain markdown: the actual instructions you want Claude to follow when the command runs.
The simplest useful version is almost nothing. The frontmatter has one line, a description, something like "fixes a GitHub issue by number." Then below the dashes you write the steps in plain English. Read the issue with the GitHub command line tool. Understand the requirements. Implement the fix. Write tests. Make a commit. That's it. You've just turned a five-step routine you'd otherwise retype every time into a single command. The instructions you wrote get injected into the conversation as if you'd typed them yourself.
And that description line isn't just a label. When you leave the door open for it, Claude can read that description and decide on its own to use the command when your request matches, which means a well-described command becomes something Claude reaches for without you naming it. We'll come back to that, because it's also a foot-gun for anything with side effects, and there's a switch to turn it off.
You get two places to keep these. If you put the command in your project's dot-claude folder, it's checked into git and everyone on the team gets it. That's where shared team workflows live, the deploy routine, the commit convention, the way your shop fixes a certain class of bug. If instead you put it in the dot-claude folder in your home directory, it follows you across every project on your machine but nobody else sees it. That's where your personal habits live. Same file format in both places. The only difference is who else gets to use it.
To see what you've got, the slash help command lists everything available, your custom commands right alongside the built-in ones. There's also a skills command that lists your skills specifically and lets you toggle which ones are visible.
Arguments, live context, and pre-approved tools
A command that does the exact same thing every time is useful. A command you can point at a specific target is far more useful. That's what arguments are for.
When you invoke a command, anything you type after the name gets handed to the command. Inside your instructions, you refer to all of it at once with a token: a dollar sign followed by the word ARGUMENTS in capitals. So if your fix-issue command says "fix GitHub issue dollar-ARGUMENTS following our coding standards," and you type slash fix-issue and then the number 4-1-2, Claude reads "fix GitHub issue 412 following our coding standards." If you want to pull arguments apart individually, you can reference them by position, dollar-one for the first, dollar-two for the second, and so on. A migrate command might take three: the component name, the framework you're coming from, and the framework you're going to. You write the instruction once with those three slots, and every invocation fills them in.
There's a field you can add to the frontmatter called argument-hint that just tells the autocomplete what to expect, something like "issue-number" in brackets, so future-you sees a reminder of what to type.
Let's make the positional version concrete, because it's where commands start feeling like little programs. Say you write a migrate-component command and declare that it takes three arguments in order: the component, the framework you're coming from, and the framework you're going to. In the instructions you write "migrate the first-argument component from the second to the third, and preserve all behavior and tests." Then you invoke it by typing slash migrate-component, then SearchBar, then React, then Vue. Claude reads "migrate the SearchBar component from React to Vue, preserve all behavior and tests." Three words on the command line expanded into a full, specific instruction, because you set up the slots once. That's the difference between a command that does one fixed thing and a command that's a reusable template you aim wherever you need it.
Now here's the feature that makes custom commands feel genuinely powerful, and it's worth slowing down for. You can run a shell command and drop its output straight into the prompt, before Claude ever sees it. You do this by starting a line with an exclamation point and then the command wrapped in backticks. When the command runs, that line gets replaced with the actual output of the shell command. So you can write a pull-request-summary command whose instructions say: here's the diff, exclamation-point and then the command that prints the PR diff; here are the comments, exclamation-point and the command that prints the comments. By the time Claude reads the command, those lines aren't commands anymore. They're the real diff and the real comments, already filled in.
This matters because it's preprocessing, not something Claude decides to do. The shell runs first, the output gets baked into the text, and Claude receives a fully rendered prompt with live data already in it. You're not asking Claude to go fetch the diff and hoping it does. You've handed it the diff. The substitution happens once over the file, and the output drops in as plain text, so a command can't print something that gets treated as another command on a second pass. If your organization wants to turn this off for safety, there's a managed setting that disables shell execution in skills entirely, and each of those lines gets replaced with a note saying execution is disabled by policy.
Two more frontmatter fields earn their keep. The first is disable-model-invocation. By default, Claude can decide on its own to use a skill when your request matches its description. That's great for helpers, and dangerous for anything with real side effects. You do not want Claude deciding, on its own, that now is a good moment to run your deploy command. So on a command that deploys, or commits, or sends a message, you set disable-model-invocation to true, and that command will only ever run when you, the human, type the slash. The model can't trigger it.
The second is allowed-tools. Normally Claude asks permission before running a shell command or editing a file, the permission system we covered two episodes back. When you list specific tools in the allowed-tools field of a command, those tools are pre-approved for the duration of that command. So a commit command can list the git-add, git-commit, and git-status commands as allowed, and while that command runs Claude executes them without stopping to ask. You're granting a narrow, scoped permission that applies only inside that one workflow, which is exactly the kind of tight blast radius you want. There's a matching field, model, if you want a particular command to run on a specific model rather than whatever you're currently using.
A worked example: the commit command
Let's put it together into something you'll actually keep. We're on a TypeScript and Next.js project, Postgres behind it, GitHub for the repo. We want a commit command that stages our changes, writes a sensible message, pushes a branch, and opens a pull request with a real description.
We make a folder named commit inside the project's skills folder, and a SKILL markdown file in it. In the frontmatter we set the description to "stage and commit the current changes and open a PR." We set disable-model-invocation to true, because committing is a side effect we want to trigger by hand. And we list the tools we're pre-approving: the git-add command, git-commit, git-push, and the GitHub PR command.
Then below the dashes, the steps in plain language. Stage the changes. Review the staged diff. Write a descriptive commit message based on what actually changed. Commit it. Push to a new branch named after the current branch. Open a pull request. And then a couple of standing instructions that lift it above a dumb macro: include a test summary in the pull request body, the number of tests run and whether they passed, and link any related issue with the word "Fixes" and the issue number so GitHub closes it automatically.
Now you type slash commit. Claude reads the diff, infers a message that describes the change instead of saying "update files," pushes the branch, and opens the PR with the body filled in. You wrote that workflow once. You'll run it a thousand times. That's the trade custom commands make: a few minutes of authoring against every future repetition.
Hooks: the guarantees the model can't forget
Here's the problem custom commands don't solve. That commit workflow says "review the diff" and "include a test summary." But nothing forces tests to actually pass before the commit lands. You're trusting Claude to do the right thing, and on the fifth turn of a long session, when context is getting crowded, Claude might just not. The instruction is advice, not a fence. We learned that distinction with permissions: CLAUDE.md is advice, permission rules are enforced. Hooks are the enforcement layer for everything else.
A hook is a script wired to a lifecycle event. When that event happens, your script runs, automatically, deterministically, every time. The model doesn't choose to run it and can't choose to skip it. So the natural question is: what are the events you can hook into?
There's a set that fire around tool use, and these are the ones you'll reach for most. There's a before-tool event, the one the docs call PreToolUse, that fires right before Claude runs any tool. Because it runs before, it can block the tool or change its input. There's an after-tool event, PostToolUse, that fires once a tool has finished successfully. It runs after the fact, so it can't un-ring that bell, but it can react, validate the result, kick off a side effect, or feed something back to Claude.
Then there are events around the shape of the conversation. There's one when you submit a prompt, before Claude sees it, which can block or add context. There's one when Claude finishes responding, the Stop event, and it can block the turn from ending, which is the trick we'll use to force tests to pass. There are session events, one when a session starts or resumes and one when it ends, good for loading context or cleaning up. There are events around compaction, when the conversation gets summarized to fit the window. There's an event for notifications, and there are events around subagents starting and stopping, which we'll care about a lot once we get into fleets later in the show. The full list lives in the hooks documentation, and it grows, so check it rather than trusting this recording in six months.
You configure hooks in the same settings file you've been editing for permissions. There's a top-level key called hooks. Under it you name the event you're hooking. Under that you give a matcher, which decides which specific cases the hook applies to. And under the matcher you list the actual handlers to run. So it's three levels of nesting: the event, then the filter, then the thing to do.
The matcher is how you avoid firing on everything. For tool events, the matcher is matched against the tool's name. You can write a single tool name to match just that one. You can write two names separated by a vertical bar to mean "either of these," like edit-or-write to catch both ways Claude changes a file. You can use a regular expression for anything fancier, including matching tools that come from MCP servers by their prefix. Those server tools have long machine names, so a pattern that matches everything from your memory server, or every write-style tool across every server, is a regular expression against that prefix. We'll lean on that hard once MCP servers come into the picture later in the show. And an empty matcher, or a star, means match everything. Being specific here isn't just tidiness; a hook that runs on every tool call adds drag to every action, so you want it firing only when it's relevant.
It's worth noting the matcher means different things for different events, because the events themselves are about different things. For a session-start hook, the matcher isn't a tool name at all; it's the reason the session started, whether you launched fresh, resumed an old session, cleared the context, or came back from a compaction. So you can run one bit of setup on a brand-new session and a different bit when you resume. The rule of thumb is that the matcher filters on whatever the event is fundamentally about, and for the tool events that happens to be the tool name.
What a hook receives and what it says back
The handler itself, the most common kind, is just a shell command. Claude Code runs it. But two things make hooks more than fire-and-forget scripts: what the hook gets handed when it runs, and how it talks back.
When a hook fires, Claude Code feeds it a blob of JSON on standard input. That JSON carries the session details and, importantly, the specifics of what triggered it. For a before-tool hook, that includes the tool's name and the exact input, so for a shell command you get the actual command string, and for a file edit you get the path being written. Your script reads that JSON, picks out the field it cares about, and decides what to do. In practice you pipe it through a small JSON tool to pull out, say, the command or the file path.
Talking back works through two channels: the exit code and whatever JSON the script prints. The exit codes are simple. Zero means success, and Claude Code reads any JSON you printed. Exit code two is the special one: it means block. The action gets stopped, and whatever your script wrote to its error output gets shown so Claude understands why. Any other non-zero code is a soft error, logged but not blocking.
For finer control you print a small JSON object. The useful part is a permission decision: your hook can say allow, or deny, or ask the human, and attach a reason that gets surfaced. A before-tool hook can even hand back a modified version of the tool input, rewriting the command before it runs. So a hook isn't just a yes-or-no gate. It can reshape what's about to happen.
There's also a field for adding context, a way to inject a note straight into the conversation, and this one's quietly powerful. A session-start hook can run, look at your repository, and tell Claude "you're on the main branch, there are three uncommitted files, the last deploy was Tuesday" the moment a session opens, so Claude starts grounded in the real state of your project instead of asking. You can even hand back things like a title for the session or a list of files to watch. The same context channel works the other direction on a post-tool hook: run your test suite after an edit, and if something fails, feed the failure summary back as context so Claude sees "two tests failed, forty-five passed" and goes to fix them, without you ever typing a word. The hook saw the problem and told Claude about it on its own.
Three hooks worth stealing
Let's make this concrete with the same project. Three hooks, each solving a real problem.
First, lint and format after every edit. We hook the after-tool event, set the matcher to edit-or-write so it only fires when Claude changes a file, and the command runs ESLint with the fix flag and then Prettier on the file that just changed. Now every time Claude touches a TypeScript file, it gets linted and formatted automatically. You never have to ask. You never have to remember. And because the file path comes in on that JSON input, the hook knows exactly which file to run against. The formatting just happens, quietly, on every edit, forever.
Second, protect the migrations folder. Database migrations are the kind of thing that's safe to write once and catastrophic to rewrite after they've run in production. So we hook the before-tool event, again matching edits and writes, and point it at a small script. The script reads the JSON input, pulls out the file path, and checks whether it's inside the migrations folder. If it is, the script prints a deny decision with a helpful reason, something like "migrations are read-only after deploy, create a new migration instead," and exits with code two. Claude physically cannot edit those files. If it tries, it gets bounced with a message that tells it the right thing to do instead. That's a guarantee, not a hope. No instruction in CLAUDE.md gives you that.
Third, and this is the one that closes the loop on our commit command: force the tests to pass. We hook the Stop event, the one that fires when Claude tries to finish its turn, and we run the test suite. If the tests fail, the hook blocks, and Claude can't wrap up. It's pushed right back into fixing what it broke. Remember the gap we found earlier, where the commit workflow asked for a test summary but couldn't enforce one? This is the fix. The slash command coordinates the work. The Stop hook makes the guarantee. Neither one alone is enough; together they're a workflow you can actually trust to run unattended, which is the whole direction this show is heading.
When to reach for which
So you've got two tools, and a clean rule for choosing between them.
Reach for a custom slash command when you have a repeatable, multi-step routine that you want to kick off deliberately. Deploying. Committing. Fixing a class of issue. Anything where you want Claude to orchestrate several steps, where you want to control the moment it runs, and especially where there are side effects you want behind a manual trigger. The command is the front door to a workflow.
Reach for a hook when something has to happen every time, with zero exceptions, and you can't afford to trust the model to remember. Linting after edits. Blocking writes to protected paths. Injecting context at session start. Running a side effect silently in the background that Claude doesn't even need to see. Forcing a check before a turn can end. If your sentence has the words "always" or "never" in it, that's a hook, not an instruction.
And the real power move is combining them, which our example already showed. The commit command is the user-facing coordination. The lint hook and the migrations hook and the test hook are the rules running underneath, enforcing the things the command merely asks for. The command is what you do. The hooks are what's true regardless. As we climb toward automated, unattended runs over the next dozen episodes, that division is going to keep paying off: the more you want to leave Claude alone, the more of your intentions need to live in hooks rather than in prose it might forget.
The pitfall, and how you'll know you hit it
Here's the one mistake almost everybody makes early, and exactly how to recognize it.
You write a custom command, you put an important rule inside it in plain English, and you assume the rule will hold. "Always run the tests before committing." "Never touch the production config." It works in your first few tries, so you trust it. Then one day, in a long session with a crowded context, Claude skips the rule. It commits without running tests. It edits the file you told it to leave alone. And you're confused, because you clearly wrote the instruction down.
The tell is this: you find yourself writing the words "always" or "never" inside a slash command. The moment you do that, you've identified something that should be a hook. A rule written in a command's instructions is advice, the same way CLAUDE.md is advice. It's a strong nudge, and most of the time the model follows it, but it is not a fence. If the cost of the model forgetting is real, a bad commit, a clobbered migration, a leaked secret, then that rule doesn't belong in prose. It belongs in a hook that runs no matter what. The fix for our test example was exactly that move: take "always run the tests," which lived as a hopeful line in the commit command, and turn it into a Stop hook that physically blocks the turn until they pass.
A few smaller traps worth knowing while we're here. Watch your hook timeouts: a lint hook with a five-second limit will start failing the moment your codebase grows, and suddenly edits are getting blocked for a reason that has nothing to do with the code. Command hooks default to a generous limit if you don't set one, so usually you can leave it alone, and for slow things you can mark a hook to run in the background instead of making Claude wait. Quote your arguments: if a command pastes whatever the user typed straight into a shell line without quoting it, you've opened a shell-injection hole, so wrap arguments in quotes or, better, use the form that passes them as a separate list instead of building one big shell string. And write descriptions that name the actual problem: a command described as "helpful utility" will never get auto-invoked, because Claude has nothing to match against, while one described as "fix a GitHub issue by reading it, implementing the fix, and opening a PR" gets picked up the moment you mention fixing an issue.
Browsing what you've built
Two commands keep you oriented as your collection grows. The hooks command opens a read-only browser of every hook that's currently configured, showing the event, the matcher, the type, and where each one came from, which is invaluable when a hook fires and you're not sure which settings file it lives in. And the skills command lists your custom commands and lets you toggle their visibility. When in doubt about what's even available, plain old slash help shows everything, built-in and custom together.
The full details, every frontmatter field, every hook event, the exact shape of the input and output JSON, live in two pages of the official docs: the skills documentation for custom commands and the hooks documentation for hooks. This is fast-moving software, so when you sit down to build, read those rather than trusting a six-month-old podcast on the exact field names.
That's the rung for today. You came in able to use the commands Claude Code ships with. You leave able to write your own, and to wire up the automatic guarantees that the shortcuts can't promise on their own. Next time we go deeper on skills as a richer primitive, the kind Claude reaches for on its own and that can bundle whole scripts and reference material, and then on handing a piece of work off to a subagent. The laptop's not in the drawer yet. But you just built the first two pieces of machinery that will eventually let it be.