Skip to content

Deduplicate Claude Code slash command definitions across installers #516

@backnotprop

Description

@backnotprop

Problem

The Claude Code slash command definitions (plannotator-review.md, plannotator-annotate.md, plannotator-last.md) exist in four places in the repository:

  1. Canonical source: apps/hook/commands/*.md — used by the Claude Code plugin install path.
  2. Duplicated inline in scripts/install.sh — hand-typed heredoc blocks, lines ~313-360.
  3. Duplicated inline in scripts/install.ps1 — hand-typed here-string blocks, lines ~225-275.
  4. Duplicated inline in scripts/install.cmd — hand-typed echo blocks, lines ~326-375.

The three installer copies must be kept in sync manually with the canonical version, and there is no CI check that enforces this. If someone edits apps/hook/commands/plannotator-review.md (to adjust the prompt, add a flag, update the description), the three installer copies silently continue shipping the old content on every subsequent install. The drift is invisible until a user reports behavior mismatch between plugin-installed and script-installed plannotator.

Visible symptom that exposed the latent duplication

PR #512 found that install.cmd was writing the Claude Code slash command files with the ! shell-invocation prefix stripped — a cmd.exe enabledelayedexpansion parser quirk — which made the files silently no-op when invoked in Claude Code on Windows cmd. The three-character echo ^! fix shipped in #512 closes the visible Windows bug but doesn't address the underlying duplication. The same class of bug could recur anywhere any of the three installers touch these inline copies.

Proposed approach

Replace the three installers' inline slash command definitions with a fetch from the canonical apps/hook/commands/ directory at install time. Two feasible paths:

  1. Reuse the existing git clone --depth 1 --sparse mechanism. The installers already sparse-checkout apps/skills/ for the skills install step. Expand the sparse pattern to also include apps/hook/commands/, then copy those files to \$CLAUDE_COMMANDS_DIR/. Requires git on the user's machine (already the case for the skills path; other code paths gracefully skip).
  2. Fetch from raw.githubusercontent.com at the installed tag. The installer knows \$latest_tag at this point and can curl -fsSL https://raw.githubusercontent.com/backnotprop/plannotator/<tag>/apps/hook/commands/<file>.md -o ... per command file. Adds network calls but works without git.

Approach 1 is preferred because it reuses existing infrastructure and doesn't add a new authenticated fetch surface.

CI changes required

Add an assertion to the Windows integration job (.github/workflows/test.ymlinstall-cmd-windows job) and equivalent checks for the other installers if possible:

  • After install, read back each generated .md file from \$CLAUDE_COMMANDS_DIR/ and compare it byte-for-byte against apps/hook/commands/<same-file> from the checked-out repo. Any drift fails CI.

This is stronger than the cycle-6 fix in PR #512, which only asserts each file contains the ! prefix — a byte-equality check catches every future class of divergence, not just the one we've seen.

Acceptance criteria

  • apps/hook/commands/*.md is the single source of truth for Claude Code slash command definitions.
  • scripts/install.sh, scripts/install.ps1, and scripts/install.cmd fetch (not inline-duplicate) these files during the Claude Code slash command install step.
  • CI asserts post-install slash command files are byte-equal to apps/hook/commands/*.md on at least the Windows cmd integration path.
  • Same treatment considered for OpenCode slash commands and Gemini slash commands (lower priority — OpenCode/Gemini command content is less likely to change and has different formats).

Out of scope

  • Adopting fixture-file architecture for the Gemini settings merge (separate concern, different fragility).
  • Migrating the skills install path itself to a different mechanism.
  • Changes to the Claude Code plugin install path (which already uses the canonical apps/hook/commands/ files correctly).

Related

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