# Custom slash commands in Claude Code: how they work now that commands are skills

> Source: <https://dev.to/rulestack/custom-slash-commands-in-claude-code-how-they-work-now-that-commands-are-skills-425k>
> Published: 2026-07-23 00:29:43+00:00

You type `/fix-issue 123`

and Claude Code expands it into a full prompt with your team's standards attached. Custom slash commands are the cheapest automation in Claude Code — one markdown file, zero config — and they changed in a way that's easy to miss: **custom commands have been merged into skills.** Your `.claude/commands/`

files keep working, but the merged model explains several things that look like bugs (arguments not expanding, tool grants vanishing mid-task) and adds features commands never had.

Here is the current behavior, verified against the official docs as of late July 2026.

Drop a markdown file into your project:

``` php
<!-- .claude/commands/fix-issue.md -->
---
description: "Fix a GitHub issue by number"
---
Fix GitHub issue $ARGUMENTS following our coding standards.
Read the issue with `gh issue view`, locate the relevant code,
implement the fix, and add a regression test.
```

Typing `/fix-issue 123`

sends that content with `$ARGUMENTS`

replaced by `123`

.

The merge means this file and a skill at `.claude/skills/fix-issue/SKILL.md`

both create `/fix-issue`

and behave the same way. What a skill adds on top:

If a command and a skill share a name, the skill takes precedence. My practical rule: throwaway personal shortcuts can stay in `commands/`

; anything you will grow or share belongs in `skills/`

. (How to write a description that actually triggers is its own topic — I covered it in [the SKILL.md guide](https://dev.to/rulestack/skillmd-how-to-write-a-claude-code-skill-that-actually-triggers-format-template-27cp).)

`$ARGUMENTS`

, `$0`

, and the quoting rules
Three expansion rules cover almost every confusion I see:

**1. $ARGUMENTS is the whole string as typed.**

`/fix-issue 123 high-priority`

→ `$ARGUMENTS`

becomes `123 high-priority`

.**2. Indexed access is zero-based and shell-quoted.** `$ARGUMENTS[0]`

(or the shorthand `$0`

) is the *first* argument. Quoting groups words:

``` php
/migrate-component "search bar" React Vue
→ $0 = search bar, $1 = React, $2 = Vue
```

**3. Missing values behave differently by placeholder type.** An indexed placeholder with no matching argument (`$2`

when you passed one arg) stays in the text *unchanged*. A named placeholder declared in frontmatter expands to an *empty string*. If you need a literal dollar-digit in prose (`$1.00`

), escape it: `\$1.00`

.

One more safety net: if you pass arguments but the file contains no `$ARGUMENTS`

at all, Claude Code appends `ARGUMENTS: <your input>`

to the end, so your input is never silently dropped.

`!` command``

This is the feature that turns a canned prompt into a grounded one. Lines like:

```
## Current diff
!`git diff HEAD`
```

run the shell command *before* Claude sees anything, and the output replaces the placeholder. Claude receives the actual diff, not an instruction to go fetch it — one fewer tool round-trip, and no chance of the model "summarizing" a diff it never read.

Substitution is a single pass: a command's output cannot emit another `!`…``

placeholder for a second expansion. Treat it as data, not macros.

`allowed-tools`

is a one-turn grant, not a session setting
The most common surprise in the merged model. Frontmatter like:

```
allowed-tools: Bash(git add:*), Bash(git commit:*)
```

pre-approves those tools **for the turn that invokes the skill**. The grant clears when you send your next message — even though the skill's *content* stays in context. So "why is Claude asking permission again for the same command?" is usually this: the instructions persisted, the grant did not. Re-invoking the skill re-applies it.

Two related notes:

`allowed-tools`

doesn't restrict anything. Unlisted tools remain available under your normal permission settings.`${CLAUDE_SKILL_DIR}`

expands in both the body and `allowed-tools`

, so a skill can ship a script and pre-approve exactly that script's invocation — no prompt, no wildcard.If you want a grant that survives the whole session, that belongs in your permission rules instead — the allow/deny/ask matching logic is [its own minefield](https://dev.to/rulestack/claude-code-permission-rules-how-allow-deny-and-ask-actually-1cid).

By default both you and Claude can run any skill. Two frontmatter switches change that:

`disable-model-invocation: true`

— only you can trigger it. Use for anything with side effects where timing matters: `/deploy`

, `/commit`

, `/send-release-notes`

. You don't want the model deploying because the code "looks ready".`user-invocable: false`

— only Claude can load it. Use for background knowledge that isn't a meaningful action, like a `legacy-system-context`

skill.Skill *descriptions* are always in context so Claude knows what exists; the *body* loads on invocation.

Once invoked, the rendered content stays in the conversation for the rest of the session (re-invoking an identical skill adds a short "already loaded" note rather than a duplicate). Two consequences:

After context compaction, recent skills are re-attached within a fixed token budget, so a long session can silently drop older ones — details in [what survives compaction](https://dev.to/rulestack/what-survives-compaction-in-claude-code-and-how-to-keep-your-rules-alive-564p).

`$0`

is the `$N`

stays literal; unmatched named args become empty`!` command``

runs before Claude reads anything; single pass`allowed-tools`

clears on your next message`code-review`

) `.claude/commands/`

still works; skills are the recommended path*I maintain Rulestack — versioned rules & skills packs for Claude Code, Cursor, and Codex, kept in sync with changes like this one: https://rulestack.gumroad.com?ref=devto*

*Daily notes on AI coding workflows live on Bluesky: https://bsky.app/profile/ai-shop.bsky.social*
