claudeplugins.
claudeplugins8 min read

Claude Code Slash Command vs Skill: When Each Fits

Slash commands wrap one-shot prompts; skills package multi-step workflows with tool allowlists. Pick by argument shape, state, and reuse.

Claude Code Slash Command vs Skill: When Each Fits

Claude Code ships two extensibility surfaces that look similar from the outside but solve different problems. A slash command is a markdown file that expands into a prompt. A skill is a self-contained directory with a YAML contract, a procedure, and an allowlist of tools. Pick the wrong one and you either fight $ARGUMENTS parsing forever or pay the startup cost of a full skill for a one-liner.

This piece compares them on the four dimensions that actually matter when you sit down to build: argument handling, multi-step orchestration, tool scoping, and reuse across projects.

The 30-second version

DimensionSlash commandSkill
File shapeSingle .md in .claude/commands/Directory with SKILL.md (frontmatter + body)
TriggerExplicit /name from userExplicit /name, or model auto-invocation via description
Arguments$ARGUMENTS string substitutionFree-form args field, parsed by the skill body
ToolsInherits parent contextCan declare allowlist (allowed-tools)
State across turnsNone \u2014 one-shot expansionCan run multi-step procedures, call sub-tools
ReusePer-project or global ~/.claude/commands/Per-project, plugin, or ~/.claude/skills/

If your command is "expand a template, fill in one variable, send to the model" \u2014 slash command. If it's "run procedure X with tool constraints, possibly across several tool calls" \u2014 skill.

Slash commands: prompt templates with $ARGUMENTS

A slash command is a file like .claude/commands/review-pr.md:

---
description: Review the diff on the current branch
---

Review the working-tree diff. Flag:
- Unhandled errors
- Missing tests for new branches
- $ARGUMENTS

End with a one-line PASS/FAIL.

When the user types /review-pr accessibility regressions, Claude Code substitutes $ARGUMENTS with accessibility regressions and feeds the resulting prompt to the model. That's it. No tool gating, no procedure, no persistent state between invocations.

The strength is that everything is in one file you can cat and reason about. The weakness is that $ARGUMENTS is a single string. Splitting it into named fields is on you \u2014 typically with a sentence like "the first word is the target file, the rest is the message." That works for two arguments. It collapses at four.

A pragmatic test: if you find yourself writing parsing instructions inside the markdown ("parse $ARGUMENTS as a JSON object with keys X, Y, Z"), you've outgrown slash commands. Move to a skill.

Skills: procedures with tool allowlists

A skill is a directory:

.claude/skills/audit-deps/
\u251c\u2500\u2500 SKILL.md
\u2514\u2500\u2500 helpers/
    \u2514\u2500\u2500 parse_lockfile.py

The SKILL.md carries YAML frontmatter and a procedure body:

---
name: audit-deps
description: |
  Audit dependency tree for known CVEs. Triggers on "audit deps",
  "check vulnerabilities", or any package-lock / Cargo.lock / uv.lock change.
allowed-tools:
  - Bash(uv pip list)
  - Bash(cargo audit)
  - Bash(bun pm ls)
  - Read
  - Grep
---

# Audit Deps

1. Detect package manager from lockfile in CWD
2. Run the matching audit command via Bash
3. Parse output, group by severity
4. Read the top 3 offending package source files for context
5. Emit a markdown table: package | version | severity | CVE link

Three things this skill gives you that a slash command cannot:

  • Allowlist gating. The allowed-tools field constrains what tools Claude can call inside this skill's scope. If a step accidentally tries Bash(rm -rf), the harness refuses before the command runs. This is the same allowlist primitive .claude/settings.json exposes globally, but scoped per skill.
  • Auto-invocation via description. Skills can fire when the model decides the description matches the current request, not just when the user types /audit-deps. This is closer to a tool the model picks up than a macro the user expands.
  • Multi-step orchestration with reads and shell calls woven together. A slash command can describe a procedure, but it can only output text. A skill can read files, run commands, and shape the output across several tool turns.

The cost is that skills are heavier to author. You need to write a description specific enough to disambiguate from other skills, and you need to think about which tools the procedure actually needs versus which it accidentally inherits.

Argument handling: where the divide gets sharp

This is the single most useful heuristic. Look at how the invocation shapes the input.

Slash command invocations look like /cmd <free-form string>. Whatever follows the command name lands in $ARGUMENTS as one blob. You can mention parsing rules inside the prompt, and the model will mostly comply, but you're asking the LLM to do structural parsing every invocation. For one or two fields, fine. For positional + flags + optional context, fragile.

Skill invocations come with an args field that the body interprets however it wants. You can write a skill where step 1 is "parse args as YAML" or "treat args as a colon-separated triple of NICHE:SLUG:KEYWORD." Because the body is procedure-shaped, you can validate, default, and error-message in ways slash commands can't reach.

A working rule: more than two distinct argument fields, or any structured argument (JSON, YAML, key=value pairs), is a skill. Single-string suffix is a slash command.

Multi-step skill workflows: what state actually means

Neither slash commands nor skills have memory across separate /invocations. The "multi-step" advantage of skills is within a single invocation \u2014 the procedure can interleave several Read, Grep, Bash, and Edit calls before producing final output.

A concrete example. A /release-checklist slash command can list 12 things to check. A release-checklist skill can:

  1. Run git log --since=<last tag> to enumerate commits
  2. For each commit, Read the touched files to flag schema changes
  3. Run bun run typecheck and cargo check to validate the workspace
  4. Aggregate findings into a markdown report
  5. If any check fails, suggest a fix command

That's five tool calls minimum, and the skill body coordinates them. A slash command would degenerate into "please run the following 12 commands" and rely on the model to thread the results \u2014 which works but is 3-5\u00d7 more token-heavy and less reliable as the procedure grows.

When the procedure has any conditional logic ("if cargo check fails, also run cargo clippy"), it belongs in a skill.

Tool scoping: the underrated feature

The allowed-tools field on a skill is the cheapest defense-in-depth you'll add to your Claude Code setup. A skill that audits dependencies should not be able to write files. A skill that drafts a blog post should not be able to run shell commands.

allowed-tools:
  - Read
  - Grep
  - Glob
  - WebFetch

This is a writer-style skill \u2014 read-only filesystem, no shell. Even if the model decides midway through the procedure that it wants to "just quickly run npm install," the harness blocks the call. You don't have to trust the model's restraint; the configuration enforces it.

Slash commands have no equivalent. They inherit whatever tool permissions the current session has. If the session has Bash(*) allowed, the slash command runs with full shell. This is fine for prompts you've authored yourself, less fine for prompts you've copy-pasted from a teammate.

For any skill that touches credentials, secrets, deployment pipelines, or destructive operations, declare an allowlist tighter than the session default.

Reuse: project, plugin, or global

Both surfaces support three placement scopes:

  • Project-local \u2014 .claude/commands/*.md or .claude/skills/*/SKILL.md checked into the repo. Best for repo-specific workflows: deploy scripts, codegen, project-bespoke linters.
  • Global \u2014 ~/.claude/commands/ or ~/.claude/skills/. Best for personal workflows you carry across projects.
  • Plugin \u2014 distributed as part of a Claude Code plugin, namespaced plugin:name. Best for capability bundles you ship to other developers.

Slash commands at the project scope are version-controlled prompts. Skills at the plugin scope are closer to npm packages: they have a description that drives auto-invocation, a contract you publish, and tool requirements you must keep stable across versions.

For a team, the practical pattern is: project-local slash commands for one-shot helpers, project-local or plugin-distributed skills for procedures everyone needs to run consistently.

Decision flow

A short flowchart that captures 90% of real cases:

  1. Does the user invoke this with at most one free-form argument? Yes \u2192 slash command.
  2. Does the procedure need more than one tool call to produce its output? Yes \u2192 skill.
  3. Should the harness restrict which tools the procedure can use? Yes \u2192 skill.
  4. Should the model decide to fire this without an explicit /name typed by the user? Yes \u2192 skill.
  5. None of the above? Default to slash command \u2014 it's the cheaper format to maintain.

The escape hatch in either direction is real. A slash command that grew complex enough to need parsing instructions and conditional logic is a sign you skipped step 2 or 3. A skill with a single-line procedure that just expands a template is a sign you over-engineered a slash command.

Common mistakes

  • Treating $ARGUMENTS as structured data. It's one string. If you need fields, you need a skill.
  • Declaring allowed-tools: [Bash(*), Read, Write, Edit] on a skill \u2014 that's the default. Tighten it.
  • Skipping the description field on a skill. Without a precise description, auto-invocation either never fires (description too vague) or fires on unrelated requests (description too broad).
  • Writing a skill that just emits text. If there are no tool calls in the procedure, a slash command does the same job with one-tenth the maintenance burden.

References: