anthropics / anthropics/claude-code

[BUG] Stacked skills: only the first is treated as user-invoked, so a disable-model-invocation skill in position 2+ is refused

Open
#95,469 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:skills bug has repro platform:windows
Dominant language
TypeScript
Stars
147k
Forks
24k
PR merge metrics
PR metrics pending

Description

Preflight Checklist
  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code
What's Wrong?

The 2.1.199 CHANGELOG states:

Stacked slash-skill invocations like /skill-a /skill-b do XYZ now load all leading skills (up to 5), not just the first

For skills carrying disable-model-invocation: true, this only works for the first skill. A gated skill in position 2 or later is refused with the error that it "cannot be used with Skill tool due to disable-model-invocation", and Claude is told to ask me to run it myself — even though I typed it myself, in the same message, as part of the documented stacking syntax.

The refusal message asks the user to do the exact thing the user already did. That self-contradiction is the clearest signal that the invocation lost its "user-invoked" provenance somewhere between the composer and the skill loader.

Position, not the flag, is the variable: the same gated skill loads perfectly when it is first.

Reproduction

Four fixtures, identical except for one frontmatter line. Each prints a fingerprint when loaded, so "did it load" is observable rather than inferred.

~/.claude/skills/zz-repro-gated-a/SKILL.md (and -gated-b, with B in the name and fingerprint):

---
name: zz-repro-gated-a
description: Repro fixture. Prints a fingerprint so loading is observable.
disable-model-invocation: true
---

When this skill is loaded, output this exact line verbatim as the FIRST line of your reply:

    REPRO-GATED-A-LOADED

Then output nothing else about this fixture.

zz-repro-plain-a / zz-repro-plain-b are byte-identical except they omit the
disable-model-invocation line and use REPRO-PLAIN-A-LOADED / -B- fingerprints.

Then, in a fresh session, send each of these. Use a new session per case so context does not
carry over.

Type each line by hand; do not pick anything from the slash menu. On this surface the menu
only opens for the first / in a message — typing a second / later in the same line opens
nothing (a separate, already-reported composer regression: #89398, #91337, #89628). Typing the
names manually deliberately bypasses that layer, which is what makes the results below
meaningful: the refusal still happens with the composer UI taken out of the picture.

Worth noting for triage: the gated skill is present and selectable in the slash menu when
/ is typed in first position. It is registered, resolvable and visible — it simply cannot be
loaded from position 2.

# Input Fingerprints expected Fingerprints actually seen
1 /zz-repro-gated-a /zz-repro-gated-b <prompt> GATED-A, GATED-B GATED-A only — B refused
2 /zz-repro-plain-a /zz-repro-gated-b <prompt> PLAIN-A, GATED-B PLAIN-A only — B refused
3 /zz-repro-gated-a /zz-repro-plain-b <prompt> GATED-A, PLAIN-B both
4 /zz-repro-plain-a /zz-repro-plain-b <prompt> PLAIN-A, PLAIN-B both

Cases 1 and 2 both show Failed to run skill /zz-repro-gated-b in the UI. Expanding that
block gives the full payload:

Skill zz-repro-gated-b cannot be used with Skill tool due to disable-model-invocation.
Ask the user to run /zz-repro-gated-b 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.

skill: zz-repro-gated-b
args: 請照兩個skill的指示做

(args is the trailing prompt text I typed after the two skill names; it is in Chinese
because that is what I typed.)

Two details in that payload matter more than the message itself:

  • args contains the trailing prompt, correctly separated from the skill name. The stacked
    input was parsed exactly as documented — the second /name was recognised as a skill rather
    than swallowed as literal text, and its arguments were peeled off cleanly. Parsing is not
    the problem.
  • The request arrived at the Skill tool — the path used for model invocation — which is
    precisely why the disable-model-invocation check fires and refuses it.

So the machinery successfully identified a skill I typed, extracted the arguments I typed, and
then asked me to go and type it myself.

What the matrix isolates:

  • Case 3 vs case 1 — a gated skill in position 1 loads fine. The flag alone is not the problem.
  • Case 2 vs case 4 — the failure follows the gated skill in position 2, regardless of what sits in position 1.
  • Case 4 — stacking itself works on this surface, so this is not the SDK stacking gap in #87113.

The single variable that predicts failure is gated AND not first.

Two independent layers. The slash menu failing to open for a second / (#89398 and
related) is a composer-UI bug. This report is about what happens after that layer is
bypassed by typing the names by hand: the input parses correctly — the second skill is
recognised and an invocation is attempted — and is then refused on a permission check.
Fixing the menu would not fix this; case 4 reaches the parser intact using the identical
typing method.

What Should Happen

All leading skills in a stacked invocation are typed by the user in one message, so all of
them are user invocations. disable-model-invocation: true should gate autonomous model
invocation, not positions 2..N of an explicit stacked invocation. Either every leading skill
loads, or stacking should reject the input up front with an explanation — silently loading a
partial set is the worst outcome, because the reply looks successful.

Why this is not a duplicate
  • #87113 (Stacked slash-skill invocations load only the first skill on the SDK / --print path) — different failure. There, the remaining input is passed through as the first skill's literal arguments; nothing is parsed as a skill. Here the second skill is recognised and an invocation is attempted — it is then refused on a permission check. Case 4 also shows plain stacking works fine on this surface.
  • #93761 (Skill tool cannot invoke disable-model-invocation skills) — likely the same root cause, but neither the title nor the report captures it. That report is a manually typed /to-tickets that follows a line of prose (looks great. then /to-tickets this), i.e. also not in first position, and it quotes the identical refusal text. It has no reproduction steps, is labelled duplicate, and reads as though the model tried to invoke a gated skill — which would be correct behaviour, not a bug. If maintainers agree these are one issue, please fold this report's matrix and fixtures into that one rather than closing this without them; the position variable is what makes it actionable.
  • #82237 (coordinator mode hides gated skills) and #92769 (plugin gated skills hidden from the invocable list) — both about skills being missing from listings. Here the skill is present and resolvable: it is visible and selectable in the slash menu, and it works in position 1 in the same session.
  • #89398 / #91337 / #89628 (slash menu opens only at position 0, or only once per message) — a composer-UI layer sitting above this one. Every case above was typed by hand specifically to rule it out, and case 4 succeeds using that same method, so the input is reaching the parser intact. These are worth fixing, but fixing them would leave this bug untouched.
Where it goes wrong

The error payload narrows this down further than the matrix alone can. Parsing succeeds — the
skill is identified and its arguments are peeled off correctly — and the refusal then comes
from the Skill-tool permission check, the check whose whole purpose is to stop the model
from invoking a gated skill. The invocation therefore loses its "typed by the user" provenance
somewhere between the stacking peel and dispatch: leading skills after the first appear to be
dispatched as ordinary Skill-tool calls instead of as explicit user invocations.

That also accounts for #93761, where a gated skill typed after a line of prose — again, not in
first position — produces the identical refusal text.

I have not read the implementation, so the exact handoff point is inferred from the payload
above plus the matrix, not confirmed from source.

Environment
  • Surface: Claude Desktop app → Code tab (Windows)
  • Desktop app: 2.2553.1.0 (MSIX)
  • Claude Code: 2.1.229
  • OS: Windows 11 Home (10.0.26200)
  • Skills: personal skills in ~/.claude/skills/ (not plugin skills)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the stacked slash-skill path from the leading-skill parser to the Skill tool permission check, using the fixtures under ~/.claude/skills/ and the four-case reproduction matrix. Done means every explicitly typed leading skill, including a disable-model-invocation skill after the first, loads successfully or the input is rejected before partial execution.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.