Every team has its boilerplate. At clients, every new REST controller we write follows the same shape: a @RestController with a @RequestMapping, constructor injection (never field injection), a @Slf4j logger, ResponseEntity return types, proper HTTP status codes, and a matching service interface already wired in. Oh, and Lombok. Always Lombok.
The first few times I asked Claude Code to generate a controller, I typed all of that out. Every time. A paragraph of instructions just to get the scaffolding right before the interesting work could begin.
After the fourth time, I thought: this is exactly the kind of thing a computer is supposed to remember.
That’s custom commands. You write the instructions once, save them as a Markdown file, and from that point on you invoke them with /your-command-name. Claude Code reads the file, follows the instructions, and you never type that paragraph again.
Where Commands Live
Custom commands are just Markdown files in a specific directory. There are two places you can put them, and the distinction matters.
Project commands: .claude/commands/
A commands folder inside the .claude directory at the root of your project. Commands here are project-scoped — they’re checked into version control alongside your code and shared with everyone on the team.
This is the right place for commands that encode your team’s conventions: your naming patterns, your architectural decisions, the libraries you’ve standardised on. When a new developer joins the team and opens Claude Code, your project commands are already there.
| |
Personal commands: ~/.claude/commands/
A commands folder in your home directory’s .claude folder. Commands here are global — available in every project you open, but only on your machine.
This is the right place for commands that reflect your personal workflow rather than team conventions. Things like /standup (summarise what I worked on today for the daily standup), /explain (give me a detailed breakdown of this code as if I’m new to it), or /diff-review (review the current git diff before I commit). These are personal productivity tools that have nothing to do with any specific project’s conventions.
A practical rule of thumb: if you’d put it in a team wiki, it goes in the project. If you’d put it in your personal notes, it goes in ~/.claude/commands/.
The Anatomy of a Command File
A command file is a Markdown file. The filename becomes the command name — spring-controller.md becomes /spring-controller. The content is the instruction Claude receives when you invoke it.
At its simplest:
| |
But you’ll almost always want a frontmatter block at the top:
| |
The description shows up in the /help menu and in autocomplete — it’s how you and your team will remember what the command does six months from now. The allowed-tools key pre-approves the tools Claude can use while running this command, so you’re not interrupted by permission prompts mid-execution.
A Closer Look at allowed-tools
The allowed-tools key isn’t a single on/off switch — it’s a list, and for Bash specifically, it supports scoping down to individual commands rather than granting blanket access.
The tool names map to Claude Code’s built-in tools: Read, Write, Edit, Bash, Glob, Grep, and WebFetch cover most command use cases. Listing a tool name on its own grants full access to that tool for the duration of the command:
| |
This means Claude can read any file, write any file, and run any shell command without prompting you — convenient, but broad.
Scoping Bash to specific commands. This is where it gets more useful. Instead of a bare Bash, you can restrict it to a pattern:
| |
Now Claude can run mvn test and mvn verify (and anything matching those prefixes) without a permission prompt, but anything else — rm, git push, curl — still asks for your explicit approval. For a command like /test, this is the difference between “Claude can run my test suite unattended” and “Claude can run anything it wants unattended.”
Applying this to the three commands in this article:
| |
Notice openapi.md uses Edit rather than Write — it’s modifying an existing file in place, not creating a new one. Being specific here isn’t pedantry; it’s documentation. Anyone on your team reading the frontmatter can tell at a glance exactly what a command is permitted to do, without reading the full instruction body.
A word of caution. allowed-tools is a convenience, not a sandbox. It tells Claude Code “don’t bother asking me about this,” not “verify this action is safe.” For a personal command in ~/.claude/commands/, granting broad access is your own call to make. For a project command checked into .claude/commands/ and shared with the team, treat allowed-tools with the same scrutiny you’d give a CI pipeline permission — scope it to exactly what the command needs, and nothing more. A command that only ever generates controllers has no business being granted unrestricted Bash.
A Side Note: Other Frontmatter Options
Next to allowed-tools, there are a number of other options that can be specified in a command’s frontmatter. None of them are required, but a few are worth knowing about.
argument-hint shows placeholder text in the autocomplete when someone starts typing your command — purely cosmetic, but useful on a team command nobody but you wrote:
| |
model pins a specific model to the command, overriding whatever’s active in the session. Use it both ways: force opus on something like /spring-controller where careful reasoning pays off, or force haiku on something trivial like a commit-message generator where speed matters more than depth.
| |
disable-model-invocation locks a command to manual use only — Claude can never decide to run it on its own. Worth adding to anything with side effects you want full control over, like a hypothetical /deploy command. None of the three commands in this article need it, since you’re always the one typing them.
| |
And one that isn’t frontmatter at all: alongside named parameters, you can use positional arguments — $1, $2, and so on — when the order of inputs is fixed and unambiguous:
| |
It sits between $ARGUMENTS and named parameters in terms of structure: more organised than one undivided blob, but without the self-documenting clarity of entity_name=Conference. For a command with two or three genuinely positional, never-confused values, it’s a reasonable middle ground.
Parameters: Making Commands Flexible
A command that always generates the same thing isn’t very useful. Parameters let you pass values in at invocation time.
There are two syntaxes. For named parameters, use {parameter_name} inside the command body:
| |
You invoke it like this:
| |
For simpler commands that take a single value, $ARGUMENTS captures everything you type after the command name:
| |
Invoked as:
| |
$ARGUMENTS vs Named Parameters: Choosing the Right One
Both syntaxes get values into your command, but they solve different problems, and picking the wrong one makes a command either annoying to use or fragile to maintain.
$ARGUMENTS is positional and raw. Whatever you type after the command name gets dropped in as a single block of text, unparsed. Claude Code doesn’t know or care what’s inside it — it’s your command body’s job to make sense of it. This makes it perfect for the simple case: one value, one slot.
| |
Here, $ARGUMENTS becomes that whole file path. There’s nothing to disambiguate, so there’s nothing to name.
But $ARGUMENTS falls apart the moment you need more than one piece of information. Suppose you tried to use it for the controller command:
| |
Now $ARGUMENTS is the string ConferenceController Conference ConferenceService /api/conferences, and your command body has to guess which word means what, based on position. Swap two arguments by accident, or have a teammate forget the order, and Claude either misinterprets the command or asks you to clarify — which defeats the purpose of having a one-line shortcut in the first place.
Named parameters trade brevity for clarity. Each value is explicitly labelled at the call site:
| |
It’s more typing, but it’s self-documenting — anyone reading that line (including future-you, three months from now) knows exactly what each value means without opening the command file to check the order. It’s also order-independent: entity_name=Conference controller_name=ConferenceController works exactly the same.
There’s a practical side benefit too: in the command body, {entity_name} can be referenced multiple times in different places (the endpoint path, the DTO name, the docstring), while $ARGUMENTS only ever gives you the one undivided blob — if you need the same input used three different ways, named parameters are really the only sane option.
A simple rule that’s served me well: if your command takes one piece of input and there’s no ambiguity about what it is — a file path, a class name, a git ref — use $ARGUMENTS. The moment you have two or more distinct values that could conceivably be confused with each other, switch to named parameters. The controller command has four; that’s an easy call. The /test and /openapi commands earlier only need a single file path; $ARGUMENTS is the obviously better fit there, which is exactly why I used it.
The Spring Boot Controller Command
Here’s the full command I use for generating controllers. This is the one that replaced that paragraph I used to type four times.
.claude/commands/spring-controller.md
| |
Now invoke it:
| |
And here’s what Claude Code generates:
| |
And the DTO record, generated alongside it:
| |
That’s a production-ready controller — matching our team’s conventions exactly — from a single command invocation. No boilerplate typing, no forgetting the Location header on the POST, no accidentally using field injection.
The WebMvcTest Command
Testing is the other place where boilerplate accumulates fast. Here’s a command that generates a @WebMvcTest for an existing controller.
.claude/commands/test.md
| |
Invoked as:
| |
Claude Code reads the controller, understands its endpoints, and generates a matching test class — including the right mocks, the right assertions, and named test methods that actually describe what they test.
The OpenAPI Command
The third command in our project toolkit adds OpenAPI documentation to a class that doesn’t have it yet — useful when you’re retrofitting documentation onto existing code.
.claude/commands/openapi.md
| |
This one is deliberately narrow in scope. The What NOT to change block is just as important as the What to add block — it keeps Claude from “helpfully” reformatting code that doesn’t need touching.
Tips for Writing Good Commands
A few things I’ve learned after building a handful of these:
Be explicit about what not to do. Claude Code is eager to help, which sometimes means it does more than you asked. A “do not” section is not defensive — it’s precise.
Mirror your existing code. The instruction “place the controller in the same package as existing controllers in this project” is more powerful than specifying a hardcoded package name. Claude Code will scan what’s there and match it, which means the command works across different projects.
Don’t build the command speculatively. Pick the single most-typed paragraph from your last week of Claude Code sessions. That’s your first command. Build it, use it three times, refine it. A command you’ve actually needed is worth ten you thought you might need.
Use allowed-tools to avoid permission interruptions. If your command reads and writes files, add allowed-tools: Read, Write to the frontmatter. Without it, Claude Code will prompt for permission on every file operation, which defeats the purpose of automation.
Where Custom Commands End and Skills Begin
You may have noticed something: the commands above are detailed instructions that Claude reasons through. They’re not deterministic — Claude interprets them and applies judgement. That’s what makes them powerful.
But there’s a related feature called Skills that takes this idea further. Where a command is a saved prompt you invoke explicitly, a skill is a set of instructions Claude Code can choose to invoke automatically based on what it’s working on. Skills also support a richer structure: they can include examples, decision trees, and metadata that helps Claude know when the skill applies.
Part 3 of this series covers skills in detail — including how to build the SKILL.md format, when Claude Code triggers a skill without being asked, and how skills and custom commands can complement each other in the same project.
For now, the rule of thumb is simple: if you find yourself invoking a command so often that you wish Claude Code would just know to use it, that’s a candidate for a skill.
Next up: part 6 covers MCP — the Model Context Protocol — and how to connect Claude Code to external tools like databases, GitHub, and your own internal APIs.
