Claude Code slash command

Add a custom /command to Claude Code that runs a script and feeds the output back to the model.

Claude Code slash command

Slash commands are the cheapest way to extend Claude Code. They are markdown files with frontmatter — no plugin scaffolding required. Drop one into .claude/commands/ and it shows up as /<filename> immediately.

File location

.claude/commands/release-notes.md

The file

---
description: Draft release notes from the last N commits
argument-hint: [count]
allowed-tools: Bash(git log:*), Bash(git diff:*), Read
---

You are drafting a release notes entry for the current branch.

Run:

!`git log --oneline -n ${1:-10}`
!`git diff --stat HEAD~${1:-10}..HEAD`

Then write a concise CHANGELOG entry under three headings:

### Features
### Fixes
### Internal

Rules:
- Reference issue / PR numbers if they appear in commits
- One bullet per change, no marketing fluff
- Skip the "Internal" section if there are no internal-only commits

How it works

A slash command is just a markdown file. The body becomes the prompt that gets sent to the model when you invoke the command, and the YAML frontmatter at the top configures how it behaves. The description is what shows up in the command list; argument-hint documents the expected arguments; and allowed-tools declares which tools the command is permitted to use, scoped narrowly with patterns like Bash(git log:*).

The interesting mechanic is the ! prefix. Any line that starts with !`...` is treated as a shell command: Claude Code runs it before the prompt reaches the model and splices the command's stdout into the prompt in place. So by the time the model starts working, it already has the git log and git diff --stat output inlined as context — it doesn't have to decide to run those commands itself. Positional arguments are interpolated with ${1}, ${2}, and so on, and the ${1:-10} syntax supplies a default (here, 10) when no argument is passed. The rest of the body is ordinary instructions: what to produce, how to structure it, and which rules to follow.

What you get

  • /release-notes runs with the default count of 10
  • /release-notes 25 passes 25 as ${1}
  • The ! prefix runs each shell command and inlines its stdout into the prompt before the model sees it
  • allowed-tools scopes the command's permission so accepting it once doesn't grant blanket Bash

Patterns I use daily

  • /loadme — runs git status, git diff, gh pr list --author @me, then asks Claude what's in flight. The single most useful command I've added.
  • /scrub — runs my repo's lint + typecheck + test in parallel and pipes failures back. Replaces my old git pre-commit hook.
  • /migrate-up — runs the migration tool with --dry-run, shows the plan, and only executes after Claude confirms. The Wait step is the whole point.

Project vs. user scope

| Path | Scope | |---|---| | .claude/commands/*.md | Project — committed, shared with the team | | ~/.claude/commands/*.md | User — your personal commands across every repo |

Project commands win when both exist. Use this to override a personal command on a per-repo basis.

When to use it

Reach for a slash command whenever you find yourself typing the same multi-step prompt repeatedly — gathering git state, kicking off a checklist, running a fixed sequence of checks. Because the ! prefix front-loads the shell output into the prompt, commands are at their best when the model needs concrete, up-to-the-moment context (the actual diff, the actual failing test output) rather than a description of it. The setup cost is essentially zero: a file in .claude/commands/ and the command exists.

A couple of things to keep in mind. The allowed-tools list is a real guardrail, not decoration — scope it as tightly as the command needs, since accepting the command once shouldn't hand it blanket Bash access. Shell commands behind ! run on every invocation, so keep them fast and side-effect-free; a command that mutates state should show a plan and wait for confirmation before executing, as the /migrate-up example does. And remember the body is a prompt, not a script: the model still interprets your instructions, so be explicit about the output format you want rather than assuming it will infer the structure.