Skip to content

[Bug] /ponytail never sets a mode in Claude Code: the UserPromptSubmit hook receives the SKILL.md body, not the command text #584

Description

@Zoz92

[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.jsonUserPromptSubmitponytail-mode-tracker.js

Reproduction

  1. Install ponytail 4.8.4 in Claude Code, start a session.
  2. Invoke /ponytail:ponytail (or /ponytail full) from the slash menu.
  3. Observe: no PONYTAIL MODE CHANGED output from the hook.
  4. ls ~/.claude/.ponytail-activeNo 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions