Repository navigation
[Bug]: Grok provider: $skill-name inserted by the composer picker is sent as literal text and never invokes the skill #13457
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Enable the Grok provider with a plugin-installed skill (on this machine the Grok CLI reports 97 skills, including
poteto-modefrom the pstack plugin). - Open a Grok thread in the T3 web client.
- Type
$in the composer and pick the skill from the picker. - Send the message.
- Compare with the same request typed as
/poteto-modeat the start of the message.
Expected behavior
Selecting a skill from the $ picker should invoke that skill, matching the semantics the picker advertises.
Actual behavior
The picker inserts the literal text $skill-name into the message. For the Grok provider T3 never converts it to an invocation, and the Grok CLI does not expand $name in a plain prompt, so the model receives the raw $skill-name string as prose and the skill never loads. The /skill-name form works because the Grok harness expands it.
Impact
Major degradation or frequent failure
Version or commit
T3 0.0.42 (server runtime at ~/.t3/runtime/versions/0.0.42), Grok CLI 1.0.41
Environment
Linux (EC2), T3 web client over loopback, Grok provider, go-muse-spark-1-3 model
Logs or stack traces
The two observations below are from one thread on the same machine.
-
A message beginning
/poteto-mode ...reached the model with the skill expanded. The session transcript shows the skill block injected into the user message, and the skill ran. -
A message beginning
$poteto-mode $poteto-mode ...reached the model as literal text. The session transcript shows$poteto-modeas plain prose with no skill block, and the reply treated it as prose.
A second check without T3 rules this out as a harness-side expansion gap rather than a T3-side one:
$ grok -m go-deepseek-v4-1-flash --always-approve --output-format json -p '$poteto-mode reply with the single word hi'
The model's own reasoning in the response: The "$poteto-mode" looks like a trigger phrase that isn't in my skill list. There's no such skill. The literal $poteto-mode string was delivered verbatim to the model.
Workaround
Type the skill as /skill-name at the start of the message. The composer's / menu also offers the skill, and that path invokes it correctly on Grok.
Supporting detail from the 0.0.42 runtime bundle, for whoever triages this.
- The web client inserts the picker selection as
`$${e.skill.name} `into the composer. src/provider/Drivers/CursorSkills.tsdefinesrewriteCursorSkillMentions, andsrc/provider/Layers/CursorAdapter.tsapplies it so$namebecomes/namefor Cursor sessions. There is no equivalent rewrite for the Grok provider;src/provider/Drivers/GrokSkills.tsonly performs discovery viagrok inspect --json.- The Grok provider snapshot does expose these skills to the client (the cached snapshot at
~/.t3/caches/grok.jsonlists 97 skills includingpoteto-mode), so the$picker offers them.
This is the same class of silent downgrade reported for Claude in #7795, which was closed and folded into the provider-agnostic skill model discussion (#6987). Filed as a bug because the concrete Grok behavior is a no-op invocation, not a feature gap.
Activity
juliusmarminge commentedon Sep 24, 2026
Confirmed on current
0.0.42. Picking a Grok skill from the composer sends the literal$nametext, and the Grok adapter never lowers it to the/nameform the Grok CLI expands. The CLI check in the report matches that:grokdelivers$poteto-modeas prose.The composer inserts
$namefor every provider, from both menus. Web does this inChatComposerwhen a skill row is chosen, and mobile does the same inuse-composer-command-menu. The slash-menu row is labeled/skill:name, but the inserted text is still$name. A message that actually starts with/skill-nameis the path that runs, because the Grok harness expands a leading slash command.GrokAdapter.sendTurnalso leaves that form untouched: when the text matches/^\/[^\s/]+/, it omits the runtime-instructions suffix so the command reaches the CLI on its own.Other providers already lower the same composer text:
$name.buildTurnStartParamsonly normalizes the currency prefix so the app-server sees its native skill token.$skillmention into a trailing text block that starts with/name(planClaudeSkillDispatch), which is the only place Claude Code expands a skill.$nameto/nameinline (rewriteCursorSkillMentionsinCursorAdapter).GrokAdapter.sendTurntrimsinput.inputand puts that string in thesession/prompttext block. There is no skill rewrite.GrokSkillsonly discovers the catalog viagrok inspect --json, and that catalog is what fills the$picker, so the skill is offered and then sent as prose. User docs say skill mentions work on any line (docs/user/composer.md), which is what Claude and Cursor implement and what this path misses for Grok.This is the same hole #9128 closed for Claude, kept at the adapter boundary so the harness runs the skill. The earlier cross-provider attempt in #8336 was closed in favor of that approach and did not land a Grok lowering. #7795 is the closed feature proposal; discussion #6987 is a separate "one plugin list" idea and does not track this failure.
Workaround: start the message with
/skill-name. Picking the skill row from$or/both insert$skill-name, which is the broken path.fix(server): send a leading Grok $skill mention as /skill
fix(server): send a leading Grok $skill mention as /skill
fix(server): send a leading Grok $skill mention as /skill