[Desktop] Slash commands registered by mods via $.command.register show "isn't a command here" and are missing from the / menu
What happens
A mod (function-hook plugin) registers a slash command with $.command.register in its session.start hook. In the desktop app's Code tab, typing that command shows the orange warning "/kit isn't a command here." in the composer. The command still runs: the plugin's command.run hook answers, the pane opens and the text output prints. So the warning is wrong, and the command is also missing from the composer's / menu.
Steps to reproduce
- Install a mod that registers a command at session start, for example:
``sh`
claude plugin marketplace add effectustasi/kit
claude plugin install kit@kit
await $.command.register({ name: 'kit', description: '…' })
It calls in on('session.start', …), and serves it with on('command.run', { command: 'kit' }, …)`.
- Open a new Code session in the Claude desktop app (Windows).
- Type
/kitand send it.
Expected: /kit is listed in the / menu and accepted without a warning, as the $.command.register docs say ("listed in the typeahead from the next keystroke on").
Actual: the composer shows "/kit isn't a command here." The command runs anyway.
What we tried
We added a markdown command commands/kit.md to the plugin so the name would be known at startup. It was listed only as /kit:kit, and it went to the model as a prompt instead of reaching the plugin's command.run { command: 'kit' } hook. So a static command can't be used to work around this.
Guess
The desktop composer seems to validate against the command list it receives when the session starts, before mods' session.start hooks finish registering. It doesn't pick up commands registered later.
Environment
- Claude Code 2.1.286 (bundled with the desktop app); CLI 2.1.287
- Claude desktop app, Code tab, Windows 10 Pro
- Reproduction: https://github.com/effectustasi/kit
- Related: #91870 (Mods)
---
_Generated by Claude Code_
3 Comments
Same on macOS. I have some extra data that may help narrow it down.
Environment: Claude desktop app 2.26454.2, Claude Code 2.1.293, macOS 26.7, Code tab, "No folder" session.
Repro mod:
claude plugin marketplace add arasovic/claude-code-mods, thenclaude plugin install show-me@claude-code-mods. It registers/show-mewith$.command.registerinsession.start.What I see
| | Mod command (
/show-me) | File-based commands (/zoom-outskill,/ponytail:ponytail-gain) ||---|---|---|
|
/menu before the first message | missing | listed || Warning on the first message | shown | not shown |
|
/menu after the first reply | listed | listed |So here the composer does list the registered command after the session's first reply. The wrong warning comes from the command list it uses before the session is live.
Likely cause (inferred from the desktop app's bundled code, not confirmed)
The pre-session command list appears to come from a temporary CLI query that the desktop app starts with
settings: { disableAllHooks: true }. With hooks off, no mod'ssession.startruns, so nothing calls$.command.register.The CLI side is easy to check. Start
claude --input-format stream-json --output-format stream-json --verbose --await-initializeand send aninitializecontrol request:--settings '{"disableAllHooks":true}': 147 commandsThe six missing ones are exactly the mod-registered commands (
show-me,changes,ctx,pin,unpin,timeline). File-based plugin and skill commands are in both lists.Keeping hooks off for that temporary query makes sense. A composer-side fix may be simpler, for example: do not show the warning until the session's live command list has arrived.
Same on macOS 15.7 with Claude Code 2.1.296 in the desktop app, with one difference: the refused first message does not run the command.
Mod: registers
/inboxwith$.command.registerand serves it withon('command.run', { command: 'inbox' }, …).What I see in a new local Code session:
| Message | Composer | Result |
|---|---|---|
|
/inbox demoas the first message | "/inbox isn't a command here." | Sent to the model as a prompt. The plugin'scommand.runhook does not answer, and the model starts a turn. ||
/inbox demoafter the first reply |/inboxlisted in the/menu, no warning | Runs as a command. |So on 2.1.296 the first message costs a model turn instead of only showing a wrong warning. The second half matches the table above: the command list is right once the session is live.
_Written by Claude_
Also reproduces on macOS (Claude desktop, Code tab, bundled Claude Code 2.1.299).
session.startwith$.command.register({ name, description })and answers them withon('command.run', { command }, …)./tsaglabarran the hook and opened its pane./tsaglabar,/tasks,/tasks-pane,/fm-agents) is sent to the model as plain text; the session notes "no command with that name is available". Restarting the app again does not help.AbovePromptband renders and its buttons call the same functions the commands do (a button that opens the pane works).claude plugin validateon the plugin passes and listscommand.run{command=…}hooks.So only the composer's recognition of mod-registered commands seems to be missing on Desktop.