Reusable prompts and slash commands
This is the sixth post in our series on the AI engineering toolkit. The previous one cut off the edit tool until the agent had read enough to write down a plan. This one is about something that happens well before any plan: the paragraph of instructions you keep retyping because you never got around to saving it anywhere.
Save the prompt once as a file, invoke it by name, and let the file carry placeholders that get filled in at run time. All three agents in this series ship a version of this. The syntax is close to identical between two of them. OpenAI pulled the whole feature from the third, and that gets its own section below.
The retyping problem
Every time you hand-type a paragraph you have typed before, you are retyping something that was never saved anywhere, so it never got a version history and the phrasing never got better. The seconds add up across a day of sessions, and the wording drifts too: the version you type on Friday is never quite the one you typed on Monday, and the agent has no way to know the two were meant to say the same thing.
A saved command fixes it. Type /review and the agent gets the same instructions every time, worded exactly the way you decided, living in a file you can read, diff, and improve. What you typed under deadline pressure last Tuesday never gave you that.
What all three need
Every command still needs three things to be useful for more than a single fixed prompt: a way to substitute arguments the user passes when invoking it, a way to run a shell command and splice the output into the prompt so the agent gets today’s diff directly instead of reasoning from scratch, and a way to pull a file’s contents in by reference without making the agent go read the file itself. Claude Code and OpenCode both give you all three, and Codex did too, until OpenAI removed it in favor of skills.
Claude Code
Custom commands live in .claude/commands/ for the project or ~/.claude/commands/ for everything you work on. A file’s name becomes its command: .claude/commands/review.md gives you /review. Subdirectories namespace it, so .claude/commands/deploy/staging.md becomes /deploy:staging.
---
description: Review a file for security issues
argument-hint: [file-path]
---
Current diff:
!`git diff HEAD`
Review @$1 for security issues. Focus on input validation and auth checks.
Argument substitution comes in three shapes. $ARGUMENTS expands to everything typed after the command name. $0, $1, and so on index into that same argument list, zero-based, so $0 is the first argument. And if you declare an arguments list in the frontmatter, each name on that list becomes its own placeholder, mapped to position in order.
Whitespace is the separator, same as a shell command line. Wrap a multi-word value in double quotes to pass it as one argument: /my-skill "hello world" second sets $0 to hello world and $1 to second. Multi-line input breaks this, though: a newline inside pasted text can corrupt the split before quoting even matters.
Shell output gets injected with a backtick-wrapped bang: !`git diff HEAD` runs before Claude ever sees the line, and the command’s output replaces it in place. File content comes in with an @ reference, @src/utils/helpers.ts, the same syntax you would type live in the chat prompt.
Sometime around mid-2026, custom commands and skills quietly became the same underlying feature. A file at .claude/commands/deploy.md and a skill at .claude/skills/deploy/SKILL.md both produce /deploy and behave the same way. Existing commands keep working with no changes required. Skills just add the plumbing a bare command file does not need: supporting files, control over whether you or the model can trigger it, running in a subagent.
That merge comes with a catch. Skills default to being invocable by the model itself, not just by you typing a slash command, and a bare command file inherits that default too. Nothing in a plain .claude/commands/deploy.md stops Claude from deciding on its own that the situation calls for /deploy and running it, unless you add disable-model-invocation: true to its frontmatter.
The same frontmatter can pre-approve specific tools too, with allowed-tools: Bash(git:*) letting a command run without asking permission for anything on that list. Both fields matter more the moment a command starts touching anything you would not want it to run unsupervised.
Treat a Claude Code custom command as the plain entry point into the same mechanism skills extend.
OpenCode
OpenCode splits the same idea two ways: a markdown file under .opencode/commands/ or ~/.config/opencode/commands/, or a command block in opencode.json if you would rather keep everything in one config file. The JSON form has no equivalent in Claude Code.
{
"command": {
"review": {
"template": "Review the component in @$1. Check for performance issues and suggest improvements.",
"agent": "build"
}
}
}
Positional arguments are one-based here, unlike Claude Code’s zero-based $0. Copy a command file between the two tools and every positional reference lands one off. $ARGUMENTS and the double-quote rule for multi-word values work exactly the same as Claude Code. So does the shell-injection syntax, !`npm test`, and the file reference, @src/components/Button.tsx.
Quoting used to fail specifically in the headless opencode run --command path, where a quoted argument got silently re-split on its way through the CLI parser, fixed in December 2025.
Where OpenCode differs most from Claude Code is that it never merged commands into skills. A command stays a static template invoked with /name, while a skill is something the agent discovers through a dedicated skill tool, listed by name and description and loaded in full only when the model decides it is relevant or a permission rule forces an ask or deny. That loader also reads .claude/skills/ and .agents/skills/, so a skill written for Claude Code works here without changes.
The same control Claude Code puts in frontmatter, OpenCode puts in config. A permission.skill block in opencode.json can allow, deny, or force an ask before a given skill loads, by exact name or wildcard.
Codex
Codex CLI had this too, for a while. Custom prompts were added to the CLI around August 2025. They lived at ~/.codex/prompts/*.md and ran with /prompts:name. Argument substitution came in several forms: $1 through $9, $ARGUMENTS, named KEY=value placeholders, and $$ to escape a literal dollar sign. Frontmatter supported description and argument-hint.
There was never a shell-injection or file-reference syntax, and never a project-scoped directory either, despite users asking for one for over a year. Every custom prompt was global, tied to your home directory, invisible to anyone else on the team unless you handed them the file by hand.
OpenAI deprecated the feature in late 2025 and removed it outright in CLI version 0.117.0, in March 2026. A startup warning had shipped a few weeks earlier. The removal broke a wave of setups overnight, judging by the nearly identical bug reports that landed within a day of the release. Prompts were gone and /prompts: no longer resolved, even though the files were still sitting untouched on disk.
Their answer each time was that skills provide a superset of what custom prompts did, and that Codex can convert your existing prompts for you.
A few users pointed out a gap in that answer. You had to type a custom prompt for it to run at all. A skill is different: the model can pick it up and run it on its own, without you asking.
Metadata can limit that, but the same users argued it does not fully replace the old guarantee for anything destructive enough to want typed-only control. The request for a proper explicit-only mode remains open. For anything you wanted to fire only on your own say, that guarantee disappeared along with the feature.
Comparing the three
Claude Code and OpenCode converge tighter here than almost anywhere else in this series. They share the same backtick-wrapped shell injection, the same @ file reference, the same $ARGUMENTS fallback, and the same double-quote rule for grouping a multi-word argument. The argument index still trips people up, though: port a command from one tool to the other and every positional reference comes out off by one.
Codex skipped past that disagreement entirely by removing the feature. Claude Code folded commands into skills and kept both working, a gentler path than the one Codex took when it tore commands out and left skills as the only door in. Codex’s history is a preview of a tension that has not really been settled: whether a separate command mechanism earns its keep once skills exist, or is just a second way to do the same thing with fewer features.
My own read is that OpenAI’s “skills are a superset” framing undersells what got lost. Invoking something on its own makes a model a fundamentally different tool than one that only runs when you say so, and no amount of metadata fully closes that gap.
A command that starts growing conditionals, or a checklist that only sometimes applies, is usually the sign you have outgrown the template and should reach for a skill. And once you have a command you want to keep, the next question is usually whether your team can see it too, which is really a question about how you share config in general.
Where this leaves you
I write one the moment I catch myself retyping something a second time. It does not take much: a couple of lines and a filename. Codex had a lighter version of this and pulled it in early 2026, on the bet that skills would cover the same ground.
They mostly do, except for one guarantee: a command runs only when you type it and always runs when you do, while a skill hands that same decision to the model. A workflow you want firing only on command needs different handling than one you are happy to let the agent reach for on its own.
The next post is about what happens once one of these commands does something you did not want: permissions, sandboxing, and the safety net each tool gives you to undo it.