Skip to content

/plannotator-annotate treats trailing natural language as a filename and fails #1182

Closed

Description

@technicalpickles

Problem

The Claude Code skills run the CLI through a bash-substitution prefix:

!`plannotator annotate $ARGUMENTS`

apps/skills/claude/plannotator-annotate/SKILL.md:12, and the same shape at line 12 of plannotator-review and plannotator-last.

That expands before the model sees anything, and $ARGUMENTS lands in argv unquoted and unparsed. So any invocation where I tack a few words of context onto the slash command dies:

/plannotator-annotate and give me the URL for it
  -> File not found: and

/plannotator-annotate the aim doc
  -> File not found: the

Bare invocation isn't better, it just prints usage:

/plannotator-annotate
  -> Usage: plannotator annotate <file.md | file.txt | file.html | https://... | folder/> ...

I hit this five times across three sessions before giving up and typing "use plannotator annotate for this, skill isn't working per se" so the agent would run the binary itself. Which works fine, and is what apps/skills/core/ already tells the agent to do.

Why it's shaped this way

I'm fairly sure this is deliberate. #872 ("restore /plannotator-* bash execution on Claude Code") put the bang prefix back specifically so the slash command executes the binary directly instead of depending on the model to make the Bash call. That tradeoff makes sense on its own terms. The cost is that the argument slot only accepts a literal path, and nothing tells you that.

apps/skills/core/plannotator-annotate/SKILL.md doesn't have the problem, since it uses a fenced bash block and instructs the agent to launch the command. So Codex gets the forgiving behavior and Claude Code gets the strict one, which is a surprising split once you notice it.

Possible fixes

Roughly in order of how invasive they are:

  1. Have annotate skip leading/trailing args it can't resolve as a path or URL and use the first one that resolves. Fixes it for every host at once.
  2. Keep the bang prefix but only forward the first whitespace-delimited token of $ARGUMENTS. Cheap, still strict, at least stops "and give me the URL for it" from being fatal.
  3. Make the error actionable. File not found: and gives no hint that the real problem is the shape of the slash command. Naming the resolved path and mentioning that only a path or URL is accepted would have saved me most of those retries.
  4. Drop the bang for the claude/ variant and match core/, accepting the model dependency that fix(install): restore /plannotator-* bash execution on Claude Code + harden unattended installs #872 was trying to avoid.

I'd lean toward 1 plus 3 since they help regardless of host, but happy to go whichever way you'd want it.

Environment

  • plannotator 0.25.1 (latest release), verified against main at 0935cf08
  • Claude Code on macOS
  • Skill at ~/.claude/skills/plannotator-annotate/, identical to apps/skills/claude/ on main apart from disable-model-invocation: true

Activity

  1. backnotprop commented on Aug 3, 2026

    @backnotprop
    Owner

    Good diagnosis, and you read the history right: the bang prefix came back in #872 precisely so the slash command does not depend on the model choosing to run the binary, and the cost you are describing (the argument slot silently requiring a literal path) was not surfaced anywhere. The Claude/Codex split you noticed is real and unintentional in its effect.

    Direction: your 1 plus 3, with one guardrail on 1. Skipping unresolvable tokens should be conservative: resolve each whitespace-delimited token, and proceed only when exactly one resolves to a path or URL. If two tokens resolve (say, two real files), that is ambiguous and should error naming both candidates rather than guessing. If none resolve, fall through to 3: an error that names what was tried and states that the command accepts a path, URL, or folder, which alone would have saved most of your retries.

    Doing it in the CLI's argument resolution rather than the skill template means every host gets the forgiving behavior at once, and the strict --gate --json surfaces keep their exit-code contract (a typo must stay exit 2 there, so the tolerant resolution will not apply to strict invocations).

    If you want to take this one too, it is yours; otherwise we will pick it up. The relevant code is the annotate argument handling in apps/hook/server/index.ts plus its Pi mirror, and note the repo rule that server-side changes land in both runtimes.

  2. backnotprop commented on Aug 3, 2026

    @backnotprop
    Owner

    Fix is up in #1183, following the direction above with one empirical finding worth sharing since it explains your experience exactly: a !-prefixed skill command that exits non-zero aborts the prompt before the model runs at all. That is why your agent never got a chance to self-correct; the error text never reached it. So the natural-language handoff prints to stdout and exits 0 in plain mode, which is the only combination that lands in the agent's context, while --json, --hook, and the strict gate keep their existing exit codes and machine-clean streams.

    The three tiers as implemented: exactly one token resolving proceeds directly (so annotate notes.md please just works with no model round-trip), multiple resolving tokens error naming every candidate rather than guessing, and nothing resolving emits an agent-addressed message echoing your words and the preserved flags so the agent re-runs with the concrete path. One behavior change to be aware of: annotate a.md b.md now errors as ambiguous instead of silently opening the first file.

  3. added 2 commits that reference this issue on Aug 3, 2026
    747b5ea
    46f1e8d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions