Max Nardit
← Artikel
10 mindev

Claude Code Slash Commands: die eingebauten Befehle, die du kennen solltest, und warum dein eigenes /command jetzt ein Skill ist

Behandle den commands-Ordner als Code, den Claude aufrufen kann, nicht als Makros. Prüfe, welche Dateien die Model-Invocation eingeschaltet lassen, kontrolliere in Skript-Läufen die Turn-Zahl statt dem Exit-Code zu vertrauen, und leg die Garantien für alles mit Seiteneffekten in Permissions und Hooks statt in ein Invocation-Flag.

Inhalt

Die alte Claude-Code-Seite für eigene Slash Commands liefert inzwischen ein anderes Dokument aus. Ruf code.claude.com/docs/en/slash-commands ab, und du bekommst „Extend Claude with skills“, Byte für Byte dieselbe Datei wie die Skills-Seite, mit einer Zeile weit oben: „Custom commands have been merged into skills.“ Dein Ordner .claude/commands/ funktioniert weiter. Laut Changelog hat die Zusammenlegung am Verhalten nichts geändert. Andersherum gelesen heißt das: Eine Datei in diesem Ordner war schon immer etwas, das Claude von sich aus ausführen kann, sofern du das nicht abschaltest, und sie ist es weiterhin. Und ein Skill, der einen Shell-Befehl einbindet, kann scheitern, bevor Claude ihn überhaupt liest, während claude -p mit 0 endet und den Lauf als Erfolg verbucht.

Beides stammt aus einem Wegwerf-Repository, Claude Code 2.1.319 und git 2.43.0 unter Linux, jeder Lauf nicht-interaktiv mit claude -p und Haiku. Läufe stehen mit ihrer Ausgabe da. Wo eine Aussage nur auf der Doku beruht, steht das dabei, und wo ich über beides hinaus schlussfolgere, ist das ebenfalls markiert.

Welche eingebauten Befehle von Claude Code solltest du kennen?

Die, die die Session selbst verwalten, denn die kann kein Skill ersetzen. Die Command-Referenz listet über hundert Einträge in drei Arten. Die meisten sind eingebaute Befehle, feste Logik im CLI. Ein paar sind als Workflow markiert, etwa /deep-research, das Arbeit auf Subagents verteilt. Zwanzig sind als Skill markiert: mitgelieferte Skills wie /code-review, /doctor, /debug, /loop und /simplify, laut Doku „a prompt handed to Claude“, also derselbe Mechanismus wie ein Skill, den du selbst schreibst.

Anthropics eigener Rundgang durch eine Session ist eine brauchbare Shortlist. Um ein Repository einzurichten: /init, /memory, /permissions, /mcp. Während einer Aufgabe: /plan, /model und /effort, /context, um zu sehen, was das Fenster füllt, und /compact, um es zu verkleinern, /btw für eine Nebenfrage, die nicht in der History landet. Vor dem Ausliefern: /diff für den Working Tree, /code-review für den aktuellen Diff, /security-review für deinen Branch gegen den Default-Branch auf origin (dafür braucht es dieses Remote). Wenn etwas kaputtgeht: /rewind zu einem Checkpoint, /doctor für einen Setup-Check. Laut derselben Referenz werden drei weitere wichtig, sobald du eigene schreibst: /skills listet, was geladen ist, /reload-skills übernimmt einen Skills-Ordner, der mitten in der Session angelegt wurde, und /skill-doctor meldet, was jeder Skill an Kontext kostet und wie oft er läuft.

Zwei Regeln aus der Doku ändern, wie du sie tippst. Jeder Befehl läuft nur, wenn er am Anfang deiner Nachricht steht. Für Skills gibt es eine zweite Hälfte: Nach normalem Text getippt, etwa in „go ahead and /deploy to staging“, führt der Name direkt nichts aus, gilt aber als Erlaubnis für Claude, diesen Skill beim Antworten auszuführen. Und ein Skill, den du unter dem Namen eines eingebauten Befehls schreibst, ersetzt diesen in einem lokalen Terminal, seine Aliase aber nicht.

Ist ein Custom Command in Claude Code jetzt ein Skill?

Ja, in allem außer der Ordnerstruktur. Laut Doku gilt für .claude/commands/deploy.md und .claude/skills/deploy/SKILL.md: „both create /deploy and work the same way“. Eine Command-Datei akzeptiert dasselbe Frontmatter wie ein Skill, außer name und paths. Was ein Skill-Ordner zusätzlich bietet, ist Platz für Begleitdateien: Referenzdokumente, die Claude nur bei Bedarf liest, Skripte, die es ausführt, ohne sie zu laden.

Gibt es beide unter demselben Namen, gewinnt der Skill. Ich habe eine Command-Datei und einen Skill namens zt-hello nebeneinandergelegt, der Command sollte mit COMMAND antworten, der Skill mit SKILL:

shell
$ claude -p "/zt-hello" --model haiku
SKILL

Unterordner von .claude/commands/ bilden weiterhin einen Namespace mit Doppelpunkt. Eine Datei unter .claude/commands/zt-ns/zt-comp.md reagierte auf /zt-ns:zt-comp. Skill-Ordner verschachteln sich so nicht, und das ist bei der Migration wichtig (unten).

Kann Claude eine alte .claude/commands-Datei von sich aus ausführen?

Ja, und das ist nicht neu. Der Changelog-Eintrag zu 1.0.123 hat ein SlashCommand-Tool hinzugefügt, „which enables Claude to invoke your slash commands“, und der Eintrag zu 2.1.3, der Commands mit Skills zusammenlegte, sagt, das sei „with no change in behavior“ geschehen. Geändert hat die Zusammenlegung die Beschreibung, die du liest: Eine Command-Datei ist jetzt als Skill dokumentiert, und die Beschreibung eines Skills steht in Claudes Kontext, damit Claude entscheiden kann, wann es ihn lädt. Wer den Ordner als Makros betrachtet, überliest das leicht. Ich habe Claude gebeten, die zt--Skills aufzulisten, die es aufrufen kann:

shell
$ claude -p "List the names of every skill you can invoke with the Skill tool whose name starts with zt-. Names only." --model haiku
zt-hello
zt-ship
zt-ns:zt-comp

zt-ns:zt-comp ist die Command-Datei, und sie hat überhaupt kein Frontmatter. Laut Doku fällt eine fehlende description auf die erste nicht-leere Zeile des Bodys zurück, also landet selbst eine nackte Prompt-Datei in der Liste. Als ich Claude anwies, sie über das Skill-Tool aufzurufen, bekam es Launching skill: zt-ns:zt-comp zurück und antwortete NESTED. Die vier, die in der Liste fehlen, zt-args, zt-diff, zt-diffsafe und zt-deploy, sind die, die disable-model-invocation: true setzen.

Eine Command-Datei, die du als Makro zum manuellen Auslösen geschrieben hast, ist also auch etwas, das Claude ausführen kann, wenn das Gespräch danach aussieht, dass es passt. Bei /summarize ist das harmlos. Bei einer Datei, die deployt, verschickt oder löscht, ist es Verhalten, das du vielleicht nie eingeplant hast, und nichts in der Datei selbst sagt es dir.

Wie schreibst du einen Claude-Code-Skill mit Argumenten?

Ein Ordner mit einer SKILL.md, YAML-Frontmatter zwischen ----Zeilen und den Anweisungen als Markdown. Alles nach dem Befehlsnamen kommt als $ARGUMENTS an; $0, $1 und so weiter greifen mit Shell-artigem Quoting auf einzelne Teile zu. Mein Test-Skill:

markdown
---
description: Echo arguments test
disable-model-invocation: true
---
Reply with exactly this line and nothing else: FIRST=[$0] SECOND=[$1] THIRD=[$2] ALL=[$ARGUMENTS]
shell
$ claude -p '/zt-args "hello world" second' --model haiku
FIRST=[hello world] SECOND=[second] THIRD=[$2] ALL=["hello world" second]

Das gequotete Paar blieb ein Argument, und $2, an dessen Position nichts stand, blieb wörtlicher Text, so wie es die Doku vorsieht. Drei weitere Details aus der Doku, bevor du Prosa mit Dollarzeichen schreibst:

  • Wenn kein Platzhalter die Argumente aufnimmt, hängt Claude Code sie als ARGUMENTS:-Zeile an, damit Claude trotzdem sieht, was du getippt hast.
  • Benannte Argumente kommen aus einer arguments-Liste im Frontmatter (arguments: [issue, branch] macht $issue zum ersten); ein benannter Platzhalter, dem nichts übergeben wird, wird zu einem leeren String, anders als $2 oben.
  • Ein wörtlicher Preis wie $1.00 im Body braucht einen Backslash, \$1.00, sonst wird sein $1 zu deinem zweiten Argument, falls es eins gibt.

Die zwei Felder, die du in fast jedem Skill setzen wirst, sind description (was der Skill tut und wann er gebraucht wird; description plus when_to_use wird in der Liste bei 1.536 Zeichen abgeschnitten, also gehört der wichtigste Anwendungsfall nach vorn) und argument-hint, der Platzhalter, der bei der Autovervollständigung erscheint. Zwei Variablen lohnen sich in jedem Skill, der ein Skript ausführt, weil sie gleich aufgelöst werden, egal in welchem Verzeichnis die Session gerade ist:

${CLAUDE_SKILL_DIR}    the folder that holds this SKILL.md
${CLAUDE_PROJECT_DIR}  the project root

Was verhindert disable-model-invocation in Claude Code tatsächlich?

Es hindert Claude daran, den Skill aus eigenem Antrieb über das Skill-Tool zu laden, und Claude Code setzt das selbst durch, statt das Modell um Wohlverhalten zu bitten. Mit gesetztem Flag verschwindet die Beschreibung komplett aus Claudes Kontext, deshalb fehlte zt-deploy in der Liste oben. Als ich Claude anwies, den Skill trotzdem aufzurufen, gab das Skill-Tool Folgendes zurück (aus dem stream-json-Log des Laufs):

<tool_use_error>Skill zt-deploy cannot be used with Skill tool due to
disable-model-invocation. Ask the user to run /zt-deploy themselves — it
cannot be invoked via the Skill tool. Do not replicate this skill's workflow
by other means — it is reserved for explicit user invocation.</tool_use_error>

Die Sperre selbst ist echt: Das Skill-Tool verweigert, egal was das Modell wollte. Der letzte Satz ist etwas anderes. „Do not replicate this skill’s workflow by other means“ ist Text, den das Modell liest, keine Prüfung, die der Harness ausführt, und ein Satz wird gewichtet, nicht erzwungen. Das Flag schließt den Weg über das Skill-Tool. Es nimmt nicht die Shell, die Dateien oder das Netzwerk weg, die die Schritte des Skills nutzen würden. Und selbst dort ist es nicht absolut: Laut Doku gilt /zt-deploy, nach normalem Text in deiner eigenen Nachricht geschrieben, als deine Erlaubnis, und seit 2.1.110 funktioniert das auch für Skills mit diesem Flag.

Das andere Feld erledigt die umgekehrte Aufgabe, und die Namen machen es leicht, die beiden zu verwechseln:

FrontmatterDu kannst /name tippenClaude kann ihn aufrufenBeschreibung in Claudes Kontext
(Standard)JaJaJa
disable-model-invocation: trueJaNicht von sich ausNein
user-invocable: falseNeinJaJa

user-invocable: false ist für Hintergrundwissen gedacht, etwa wie ein Legacy-System funktioniert, das Claude anwenden soll, aber niemand als Befehl braucht. Es versteckt den Skill im /-Menü und ignoriert laut Doku, wenn du seinen Namen tippst. Claude hält es von nichts ab.

Das Frontmatter-Flag ist auch nicht der einzige Schalter. Laut Doku erreicht ein skillOverrides-Eintrag dasselbe, ohne die Datei zu bearbeiten, und das ist der Weg für einen Skill, der in einem geteilten Repository eingecheckt ist. Das /skills-Menü schreibt ihn in .claude/settings.local.json, deine eigenen, nicht committeten Settings für dieses Projekt:

json
{
  "skillOverrides": { "deploy": "user-invocable-only" }
}

"off" versteckt den Skill stattdessen vor Claude und im /-Menü, und wer seinen vollen Namen tippt, bekommt einen Fehler, statt dass er läuft. Ein separater Hebel, eine Deny-Regel wie Skill(deploy *) in deinen Permission-Settings, nimmt den Skill dem Skill-Tool weg, egal was sein Frontmatter sagt.

Schränkt allowed-tools in einem Claude-Code-Skill irgendetwas ein?

Nein. Es genehmigt vorab. Die Doku ist da eindeutig: allowed-tools lässt Claude die gelisteten Tools ohne Nachfrage nutzen, „during the turn that invokes this skill“, die Freigabe verfällt, sobald du deine nächste Nachricht schickst, und „it does not restrict which tools are available“. Ein Deploy-Skill mit allowed-tools: Bash(git push *) beschränkt Claude nicht auf git push. Er entfernt die Nachfrage für git push, für einen Turn.

Das Feld, das Tools entfernt, ist disallowed-tools, und auch das gilt nur, solange der Skill aktiv ist. Dauerhafte Grenzen stehen in Permission-Regeln: Deny- und Ask-Regeln haben weiterhin Vorrang vor dem allowed-tools eines Skills.

Noch eine Eigenschaft, von derselben Seite: „Workspace trust doesn’t gate this field.“ Das allowed-tools eines Projekt-Skills gilt sogar in einem claude -p-Lauf in einem Ordner, dem du nie vertraut hast, und das gehört auf die Liste dessen, was die Settings eines geklonten Repositorys trotzdem tun können. Lies das Frontmatter der Skills in einem Repository, bevor du Claude Code darin startest.

Für alles mit Seiteneffekten hier meine Lesart der Ebenen; die Doku beschreibt jede davon, bringt sie aber nicht in eine Rangfolge. disable-model-invocation entscheidet, wer den Skill starten darf. Permission-Regeln entscheiden, welche Befehle überhaupt laufen dürfen, egal wer sie startet. Ein Hook läuft jedes Mal, wenn sein Event feuert, ob Claude dem Skill folgt oder nicht, und deshalb lautet die Lösung der Doku selbst für „Claude skipped a rule that must hold every time“, die Regel in einen Hook zu verschieben. Nimm das Erste für die Absicht, das Zweite und Dritte für die Garantie.

Warum endet claude -p mit 0, wenn der Shell-Befehl eines Claude-Code-Skills fehlschlägt?

Weil der Skill abbricht, bevor Claude auch nur einen Turn ausführt, und nichts im Headless-Lauf das als Fehler zählt. Das allererste Beispiel der Doku löst es aus: git diff HEAD schlägt in einem Repository ohne Commits fehl, und ein fehlgeschlagener eingebundener Befehl bricht den ganzen Skill ab. Dieses Beispiel beginnt mit !`git diff HEAD`, das den Befehl ausführt, bevor Claude irgendetwas sieht, und seine Ausgabe in den Prompt einfügt. In einem frisch initialisierten Repository:

shell
$ claude -p "/zt-diff" --model haiku
Shell command failed for pattern "!`git diff HEAD`": [stderr]
fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

So weit beschreibt es die Doku: Ein fehlgeschlagener Befehl bricht den Aufruf ab, und Claude sieht den Skill nie. (Mit der Standard-Shell zählt jeder Exit-Code ungleich null, außer Exit-Code 1 von Such- und Vergleichsbefehlen wie grep.) Was sie nicht erwähnt, ist, wie ein Skript-Lauf das meldet:

shell
$ claude -p "/zt-diff" --model haiku < /dev/null > /dev/null; echo "exit=$?"
exit=0
 
$ claude -p "/zt-diff" --model haiku --output-format json
{"is_error": false, "subtype": "success", "num_turns": 0, "result": "Shell command failed for pattern ..."}

Das JSON ist auf die vier Felder gekürzt, auf die es ankommt. Exit-Code 0, is_error false, subtype success. Ein Cron-Job oder CI-Schritt, der eins davon prüft, verbucht einen Erfolg für einen Skill, der nie gelaufen ist. Das Erkennungszeichen ist num_turns: 0 und ein Ergebnis, das mit „Shell command failed for pattern“ beginnt, also prüf darauf, wenn du Skills aus Skripten aufrufst.

Die Lösung im Skill ist die, die die Doku für Befehle angibt, bei denen ein Exit-Code ungleich null zu erwarten ist: || true anhängen. Damit landet gits Fehlertext trotzdem im Prompt (Claude Code führt unter der Standard-Shell stderr in die eingebundene Ausgabe mit ein, das 2>&1 unten macht es also nur explizit), und Claude kann sehen, warum der Diff leer ist:

markdown
## Current changes
!`git diff HEAD 2>&1 || true`

Derselbe Skill mit dieser Zeile wurde geladen und antwortete normal. Eine verwandte Regel aus der Doku: Außerhalb des Auto-Modus bricht ein eingebundener Befehl, den deine Permission-Regeln nicht erlauben, den Skill ebenfalls ab, statt nachzufragen. Genehmige ihn vorab in allowed-tools.

Wie verschiebst du .claude/commands-Dateien in Claude-Code-Skills?

Es gibt keine Frist: Command-Dateien funktionieren weiter. Verschieb eine Datei, wenn der Command Begleitdateien braucht. Prüf alle jetzt, wegen des Invocation-Standards von oben. Eine Abfolge, die funktioniert:

  1. Finde die Command-Dateien, die Claude aufrufen kann. Alles, dessen Frontmatter das Flag nicht einschaltet. Ein einfaches grep über die ganze Datei fällt auf ein Beispiel im Body herein und übersieht yes oder True, was die Doku akzeptiert, deshalb liest das hier nur den Frontmatter-Block:

    shell
    find .claude/commands ~/.claude/commands -name '*.md' 2>/dev/null | while read -r f; do
      awk 'NR==1 && $0!="---" {exit} NR>1 && $0=="---" {exit}
           NR>1 && tolower($0) ~ /^disable-model-invocation: *(true|yes|on|1) *$/ {found=1}
           END {exit !found}' "$f" || echo "model-invocable: $f"
    done

    In meinem Test-Repository listete es die drei Command-Dateien, die das Flag aus lassen, darunter eine, die das Flag nur im Body erwähnt, und übersprang eine, die es auf Yes setzt. Es ist eine Textprüfung: Sie sieht keinen Skill, der die Datei unter demselben Namen überdeckt, und keinen skillOverrides-Eintrag. Auf meinen eigenen persönlichen Skills-Ordner für diesen Artikel angesetzt, fand dasselbe grep dort keinen Skill, der das Flag setzt. Ob das richtig ist, hängt davon ab, was jeder Skill tut; die Prüfung legt dir nur die Frage vor.

  2. Setz das Flag bei allem mit Seiteneffekten, ob du die Datei verschiebst oder nicht.

  3. Verschieb, was einen Ordner braucht. mkdir -p .claude/skills/deploy, dann git mv .claude/commands/deploy.md .claude/skills/deploy/SKILL.md für eine getrackte Datei oder einfach mv für eine, die Git nicht trackt. Verschieben statt Kopieren ist weniger wichtig, als es scheint, weil der Skill den Namenskonflikt ohnehin gewinnen würde, aber zwei Dateien mit einem Namen sind Verwirrung, der du später begegnest.

  4. Benenne Commands mit Namespace bewusst um. .claude/commands/frontend/component.md ist /frontend:component. Ein Skill bekommt seinen Befehlsnamen von seinem Ordner oder seinem name-Feld, die verschobene Version bekommt also einen neuen Namen, den du wählst, etwa frontend-component. Wer den alten Namen in den Fingern hat, muss Bescheid bekommen.

  5. Validiere das Frontmatter. Eine Datei, deren YAML sich nicht parsen lässt, wird trotzdem geladen, mit allen Feldern stillschweigend verworfen, einschließlich des Flags, das du gerade hinzugefügt hast. Lass den Validator auf beide Ordner laufen, .claude/skills und .claude/commands, nie auf einen einzelnen Skill-Ordner (der wird als Plugin behandelt und scheitert am fehlenden Manifest):

    shell
    $ claude plugin validate .claude/skills
    Validating skill: .claude/skills/zt-broken/SKILL.md
     
    ✘ Found 1 error:
      ❯ frontmatter: YAML frontmatter failed to parse: YAML Parse error: Unexpected character. At runtime this skill loads with empty metadata (all frontmatter fields silently dropped).

    Das ist die Ausgabe für einen absichtlich kaputten Test-Skill, mit gekürztem Pfad. Auf demselben Ordner ohne ihn gab der Lauf ✔ Validation passed aus. Auf .claude/commands warnt er außerdem vor einer Command-Datei ohne Frontmatter-Block und besteht mit Warnungen.

Eine Datei in .claude/commands/ liest sich wie ein Makro, etwas, das du ausführst. Als Skill ist sie etwas, das du und Claude beide ausführen können, und beide ist der Standard. Meistens ist dieser Standard das, was du willst. Genau darum geht es bei einem Skill. Einen zweiten Blick wert sind die Dateien, die als Makros geschrieben wurden, in der Annahme, dass nur du sie jemals ausführen würdest.

01 / Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

Max Nardit@mnardit

02 / Weitere Artikel