Skip to content

fix(extensions): preserve argument-hint in extension Claude SKILL.md (#2903)#2916

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/2903-extension-argument-hint
Open

fix(extensions): preserve argument-hint in extension Claude SKILL.md (#2903)#2916
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/2903-extension-argument-hint

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Description

Closes #2903.

When a command supplied by an extension (specify extension add) declares argument-hint: in its frontmatter, that field was dropped from the generated Claude skill (.claude/skills/<name>/SKILL.md), even though core template commands keep it.

Root cause. The extension skill generator (ExtensionManager._register_extension_skills) builds the skill frontmatter via the shared CommandRegistrar.build_skill_frontmatter(), which only emits name / description / compatibility / metadataargument-hint was never forwarded. Core commands get their hint through a different mechanism (ClaudeIntegration.setup() injects from a hardcoded ARGUMENT_HINTS map), so the inconsistency the issue describes is real.

Fix (src/specify_cli/extensions.py): carry argument-hint from the already-parsed source command frontmatter into the skill frontmatter dict, before yaml.safe_dump, gated on the integration exposing inject_argument_hint:

argument_hint = frontmatter.get("argument-hint")
if (argument_hint and integration is not None
        and hasattr(integration, "inject_argument_hint")):
    frontmatter_data["argument-hint"] = str(argument_hint)

Two deliberate choices:

  • Dict injection, not the string helper. Reusing the existing ClaudeIntegration.inject_argument_hint (which inserts a line right after description:) corrupts the YAML when the description folds across lines — it splits the folded scalar:
    description: Build and maintain a lean, static context/ knowledge folder so coding
    argument-hint: "<init | update | list | check>"
      agents load only what is relevant and save tokens   # <- now an orphaned line -> yaml.ParserError
    
    Adding the key to the dict before yaml.safe_dump is always valid YAML. The new regression test uses such a folding description to lock this in.
  • hasattr(integration, "inject_argument_hint") gate. ClaudeIntegration is the only integration that defines inject_argument_hint, so only Claude receives the key — build_skill_frontmatter's shared shape is unchanged for every other skills agent (kimi, codex, etc.). This mirrors the adjacent hasattr(integration, "post_process_skill_content") gate a few lines below.

No runtime/slash-command behavior changes — this only affects the frontmatter of generated SKILL.md files.

Scope (intentionally extension-only)

Per #2903's title, body, and repro (all about specify extension add), this fix is limited to the extension install path. The same build_skill_frontmatter → yaml.safe_dump pattern also drops argument-hint in the preset skill generator (presets.py _register_skills/restore paths ~1342/1438/1475) and the init-time template-conversion path (agents.py render_skill_command ~332; see the tech-debt note at agents.py:314-318). I've deliberately left those out to keep this PR focused (per CONTRIBUTING's "keep changes focused / split independent changes") — and because the preset path is a genuinely separate problem: presets can override core commands whose hint is dict-driven (ARGUMENT_HINTS), not frontmatter-driven, so a correct preset fix needs more than a frontmatter.get("argument-hint") forward. Happy to follow up with a separate PR if you'd like.

Note on the issue's secondary claim

The issue also notes user-invocable / disable-model-invocation as "absent". Those are not dropped on current mainpost_process_skill_content injects them for Claude extension skills (extensions.py), and tests/test_extension_skills.py::test_skill_md_has_parseable_yaml already asserts disable-model-invocation is False. So this PR is scoped to the one genuine defect: argument-hint.

Related open PRs

#2776 (integration-aware command hints) and #2103 both touch extensions.py; #2103 also touches tests/test_extension_skills.py. This is a small, independently-shippable bug fix; I'm happy to rebase if either lands first.

Testing

  • Tested locally with uv run specify --help (exit 0)
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (N/A — generation-only change; covered by the new unit tests below)

Details (Windows 11, Python 3.12.12):

  • uvx ruff check src/ — All checks passed
  • uv run pytest tests/test_extension_skills.py tests/integrations/test_integration_claude.py -q83 passed, 2 skipped
  • Full suite uv run pytest3600 passed, 148 skipped, 14 failed; the 14 failures are pre-existing and environment-only (os.symlinkWinError 1314; creating symlinks needs elevation/Developer Mode on Windows), identical to clean main on this host and unrelated to this change.

New tests in tests/test_extension_skills.py:

  • test_argument_hint_preserved_for_extension_command — installs an extension command with argument-hint and a long, folding description, asserts the generated SKILL.md frontmatter parses and retains both argument-hint and the full description. (Fails on main with KeyError: 'argument-hint'.)
  • test_argument_hint_not_added_for_non_claude_agent — installs the same kind of command under a non-Claude skills agent (kimi) and asserts argument-hint is not present, proving the gate keeps the key Claude-only.

Manual sanity: this changes only the emitted SKILL.md frontmatter, so no slash command's behavior changes; the generated file parses as valid YAML and surfaces the hint in Claude Code.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

This fix was developed with Claude Code (Claude Opus 4.8) under my direction — locating the root cause, writing the change and the regression tests, and running the verification above. I personally confirmed the folded-description YAML-corruption failure mode, verified the non-Claude gate, and reviewed the diff before submitting. I'll disclose if any review responses are AI-assisted as well.

Extension-provided commands that declare `argument-hint:` in their
frontmatter had that field dropped from the generated Claude
`.claude/skills/<name>/SKILL.md`, while core template commands keep it.
The extension skill generator built the frontmatter via the shared
build_skill_frontmatter() (name/description/compatibility/metadata only)
and never forwarded argument-hint.

Carry argument-hint from the parsed source command frontmatter into the
skill frontmatter dict before serialization, gated on the integration
exposing inject_argument_hint so only argument-hint-aware agents (Claude)
receive the key and build_skill_frontmatter's shared shape stays unchanged
for every other agent. The value is injected into the dict rather than via
the string-based inject_argument_hint helper, so a folded multi-line
description cannot be split into invalid YAML.

Add regression tests covering a folding description (Claude) and the
non-Claude gate (kimi).

Closes github#2903

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jawwad-ali jawwad-ali requested a review from mnriem as a code owner June 10, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: argument-hint dropped from extension commands when generating Claude SKILL.md (kept for core commands)

1 participant