[Bug] /ponytail never sets a mode in Claude Code: the UserPromptSubmit hook receives the SKILL.md body, not the command text
Summary
In Claude Code, /ponytail is dispatched as a skill, not as a commands/*.md slash command. The harness therefore hands UserPromptSubmit a data.prompt that begins with <command-message> / <command-name> tags and then contains the entire body of skills/ponytail/SKILL.md — not the literal string /ponytail:ponytail.
hooks/ponytail-mode-tracker.js:23 tests:
if (/^[/@$]ponytail/.test(prompt)) {
That anchor never matches, so setMode() is never called, ~/.claude/.ponytail-active is never written, and everything that reads it (ponytail-instructions.js, ponytail-subagent.js, the statusline) sees no active mode.
This is the same root cause as the (now fixed) #161, seen from the other side. Before #162 landed, the old deactivation regex
if (/\b(stop ponytail|normal mode)\b/i.test(prompt)) {
matched ponytail's own SKILL.md, which says on line 29:
unsure. Off only: "stop ponytail" / "normal mode". Default: **full**.
So invoking /ponytail:ponytail printed PONYTAIL MODE OFF. The skill disabled itself by reading itself. #162's isDeactivationCommand fixed that half; the command-detection half is still open.
Environment
- ponytail 4.8.4 (marketplace
DietrichGebert/ponytail, user scope)
- Claude Code CLI 2.1.206, Linux
- Hook wired via
hooks/claude-codex-hooks.json → UserPromptSubmit → ponytail-mode-tracker.js
Reproduction
- Install ponytail 4.8.4 in Claude Code, start a session.
- Invoke
/ponytail:ponytail (or /ponytail full) from the slash menu.
- Observe: no
PONYTAIL MODE CHANGED output from the hook.
ls ~/.claude/.ponytail-active → No such file or directory.
On 4.2.0, step 3 instead prints PONYTAIL MODE OFF, every single time.
Typing ponytail ultra as ordinary prose still works, because then data.prompt really is the literal text. Only the slash/skill path is affected — which is the documented way to use it.
Why this wasn't caught
#269 reported the commands/*.toml vs commands/*.md mismatch and was closed unmerged, with:
the UserPromptSubmit hook (ponytail-mode-tracker.js) parses the prompt text, so /ponytail ultra persists the mode whether it came from a skill or was typed raw. It even handles the /ponytail:ponytail form.
The code does handle that form. The harness just never delivers it: when the command comes from a skill, the prompt is the skill body. The skills do show up in the / menu with their arg hints, as you observed — that part is orthogonal, and it's what makes the failure invisible. The menu entry works; the hook behind it doesn't.
Suggested fix
Prefer the <command-name> tag when present, and fall back to the raw prompt otherwise. Claude Code emits a <command-args> tag for at least some commands (empty when there is no argument); the snippet below tolerates its absence:
const raw = data.prompt || '';
const name = raw.match(/<command-name>\s*([^<]+?)\s*<\/command-name>/)?.[1];
const args = raw.match(/<command-args>\s*([^<]*?)\s*<\/command-args>/)?.[1];
const prompt = (name ? `${name} ${args ?? ''}` : raw).trim().toLowerCase();
Everything downstream (^[/@$]ponytail, the parts[0] / parts[1] split, isDeactivationCommand) then works unchanged, and isDeactivationCommand keeps protecting the raw-prose path.
Worth a regression test that feeds the tracker a data.prompt built from the real skill body: it fails on both counts today (no activation; and on any build predating #162, a spurious deactivation).
[Bug]
/ponytailnever sets a mode in Claude Code: the UserPromptSubmit hook receives the SKILL.md body, not the command textSummary
In Claude Code,
/ponytailis dispatched as a skill, not as acommands/*.mdslash command. The harness therefore handsUserPromptSubmitadata.promptthat begins with<command-message>/<command-name>tags and then contains the entire body ofskills/ponytail/SKILL.md— not the literal string/ponytail:ponytail.hooks/ponytail-mode-tracker.js:23tests:That anchor never matches, so
setMode()is never called,~/.claude/.ponytail-activeis never written, and everything that reads it (ponytail-instructions.js,ponytail-subagent.js, the statusline) sees no active mode.This is the same root cause as the (now fixed) #161, seen from the other side. Before #162 landed, the old deactivation regex
matched ponytail's own SKILL.md, which says on line 29:
So invoking
/ponytail:ponytailprintedPONYTAIL MODE OFF. The skill disabled itself by reading itself. #162'sisDeactivationCommandfixed that half; the command-detection half is still open.Environment
DietrichGebert/ponytail, user scope)hooks/claude-codex-hooks.json→UserPromptSubmit→ponytail-mode-tracker.jsReproduction
/ponytail:ponytail(or/ponytail full) from the slash menu.PONYTAIL MODE CHANGEDoutput from the hook.ls ~/.claude/.ponytail-active→ No such file or directory.On 4.2.0, step 3 instead prints
PONYTAIL MODE OFF, every single time.Typing
ponytail ultraas ordinary prose still works, because thendata.promptreally is the literal text. Only the slash/skill path is affected — which is the documented way to use it.Why this wasn't caught
#269 reported the
commands/*.tomlvscommands/*.mdmismatch and was closed unmerged, with:The code does handle that form. The harness just never delivers it: when the command comes from a skill, the prompt is the skill body. The skills do show up in the
/menu with their arg hints, as you observed — that part is orthogonal, and it's what makes the failure invisible. The menu entry works; the hook behind it doesn't.Suggested fix
Prefer the
<command-name>tag when present, and fall back to the raw prompt otherwise. Claude Code emits a<command-args>tag for at least some commands (empty when there is no argument); the snippet below tolerates its absence:Everything downstream (
^[/@$]ponytail, theparts[0]/parts[1]split,isDeactivationCommand) then works unchanged, andisDeactivationCommandkeeps protecting the raw-prose path.Worth a regression test that feeds the tracker a
data.promptbuilt from the real skill body: it fails on both counts today (no activation; and on any build predating #162, a spurious deactivation).