[BUG] Mods: $.command.register refused for a user/project skill's name is undocumented, says "user's" for project skills, and the debug log omits the error
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
A mod's $.command.register({ name }) is refused when the person already has a skill with that name — a user skill (~/.claude/skills/handoff/) or a project skill (.claude/skills/browse/). Three problems around this:
- Undocumented. The
command.registerdoc comment says only "a built-in's name is refused". A plugin author cannot anticipate the refusal, andclaude plugin validatecannot catch it because it depends on each installer's skills. - The reason is misleading for project skills. With a project skill named
browse, the rejection still readsit is the user's /browse. - The debug log omits the error. When the refusal throws out of a hook that has a
.catch, the debug log only recordshook failed closed: <plugin>: errorKind=HooksError errorChars=78 (session.start; its .catch answered)— the length of the message, not its text. We had to add$.ui.log(next.error.message)to the handler to see the cause.
Combined with how a .catch resumes the chain (after a hook throws before next, next(e) continues at the next plugin, skipping the failing plugin's remaining hooks for that event), one refused name made every later session.start hook of our plugin be skipped. Commands of unrelated features were never registered, and the person only saw Unknown command: /browse.
What Should Happen?
- The refusal rule (user/project commands and skills take precedence) and its message format are documented on
command.register/CommandSpec. - The reason says where the conflicting name comes from (user vs project).
- The
hook failed closeddebug line includes at least the start of the error message.
Error Messages/Logs
# debug log (interactive session): commands registered before the failing hook are "listed", then:
[WARN] hook failed closed: takgon-mods: errorKind=HooksError errorChars=78 (session.start; its .catch answered)
# message, only visible after logging next.error.message from the .catch handler:
takgon-mods: $.command.register: "/handoff" refused: it is the user's /handoff
# with a PROJECT skill .claude/skills/browse/SKILL.md, the rejection reason is still:
it is the user's /browse
Steps to Reproduce
- Have a user skill
~/.claude/skills/handoff/SKILL.md(any content). - Install a mod whose hooks module registers commands from several
session.starthooks (one per feature):
```ts
on('session.start', { isInteractive: true }, async ($, e, next) => {
await $.command.register({ name: 'handoff', description: '...' }) // refused
return next(e)
}).catch(($, e, next) => next(e))
on('session.start', { isInteractive: true }, async ($, e, next) => {
await $.command.register({ name: 'browse', description: '...' }) // never runs
return next(e)
})
```
- Start an interactive session with
--debug-file logand type/browse. - Observe
Unknown command: /browse, and in the log only thehook failed closed ... errorChars=78line. - For the project-skill wording: put
.claude/skills/browse/SKILL.mdin a project and registerbrowse; the rejection readsit is the user's /browse.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.296 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Other
Additional Information
- Reproduced in interactive sessions driven through a Windows pseudo-console (pywinpty).
- In one earlier interactive session with the same user skill present,
/handoffwas registered and answered by the plugin; we could not determine what makes the refusal conditional (load order?). - Workaround: wrap every
$.command.registerso a refusal never throws out of the hook, show one toast per refused name with the engine's reason, and rename the colliding command. - Related: #99691 (mods' registered slash commands missing on Desktop).