Skip to content

[Bug]: #3991 still reproduces on 0.16.2 — Claude ARGUMENT_HINTS fallback injects into a folded description scalar (wrap presets without explicit argument-hint) #4044

Description

@takaya0

Bug Description

#3991 (preset wrap composition drops core argument-hint and leaks its value into description) was closed by #3996, but on 0.16.2 the corruption still reproduces for the configuration that motivated the original report: Claude integration + a strategy: wrap preset whose description is long enough for the YAML dumper to fold it across multiple lines.

Two facts combine:

  1. The [bug-fix] Fix preset-wrap-drops-argument-hint: inherit argument-hint from core template #3996 inheritance fix is a no-op for bundled core commands. [bug-fix] Fix preset-wrap-drops-argument-hint: inherit argument-hint from core template #3996 adds argument-hint to the wrap-composition inheritance allowlist (copied from core_frontmatter when the preset frontmatter doesn't declare it). But the bundled core command templates (e.g. core_pack/commands/specify.md) declare no argument-hint frontmatter at all — for Claude the hints live in the hardcoded ARGUMENT_HINTS dict in src/specify_cli/integrations/claude/__init__.py. The docstring of agents.py::apply_argument_hint states this explicitly: "Built-in templates carry no argument-hint, so this is a no-op for the core path." So there is nothing to inherit, and the composed frontmatter reaches serialization without the key.
  2. The Claude ARGUMENT_HINTS fallback then injects by raw string insertion into already-serialized YAML. ClaudeIntegration.post_process_skill_content → inject_argument_hint inserts argument-hint: "…" directly after the first line starting with description:. With a short one-line description this happens to work (which is presumably why [bug-fix] Fix preset-wrap-drops-argument-hint: inherit argument-hint from core template #3996's regression test passes). With a description long enough that dump_frontmatter folds it into a multi-line scalar, the injected line lands inside the folded scalar.

Depending on whether the dumper quoted the scalar, the failure takes one of two forms (both reproduced on 0.16.2):

Steps to Reproduce

  1. Fresh project on 0.16.2:

    mkdir demo && cd demo && git init -q && git commit -q --allow-empty -m init
    specify init --here --integration claude --script sh --ignore-agent-tools --force
  2. Create a wrap preset whose command frontmatter declares only a long description (~150+ chars, so it folds when dumped) and strategy: wrap — no argument-hint:

    presets-src/demo-wrap/commands/speckit.specify.md

    ---
    description: "Create or update the feature specification from a natural language feature description. Also accepts an issue URL resolved via gh CLI (demo customization)."
    strategy: wrap
    ---
    
    <!-- demo preamble -->
    
    {CORE_TEMPLATE}

    plus a matching preset.yml with provides.templates: [{type: command, name: speckit.specify, file: commands/speckit.specify.md, strategy: wrap}].

  3. Apply and inspect:

    specify preset add --dev ./presets-src/demo-wrap
    head -8 .claude/skills/speckit-specify/SKILL.md
    python3 -c "import yaml; print(yaml.safe_load(open('.claude/skills/speckit-specify/SKILL.md').read().split('---')[1]))"

Expected Behavior

The composed frontmatter contains the preset's description exactly as declared, plus argument-hint: Describe the feature you want to specify as its own key (from the Claude ARGUMENT_HINTS mapping).

Actual Behavior

With the description above (plain scalar), the composed frontmatter is invalid YAML:

name: speckit-specify
description: Create or update the feature specification from a natural language feature
argument-hint: "Describe the feature you want to specify"
  description. Also accepts an issue URL resolved via gh CLI (demo customization).
yaml.parser.ParserError: while parsing a block mapping
  ...
expected <block end>, but found '<scalar>'

If the description contains a character that forces quoting (e.g. change it to ... Also accepts a GitHub issue/PR URL or #N reference resolved via gh CLI (demo).), the output parses but is silently corrupted:

description: 'Create or update the feature specification from a natural language feature
argument-hint: "Describe the feature you want to specify"
  description. Also accepts a GitHub issue/PR URL or #N reference resolved via gh
  CLI (demo).'

Parsed result: no argument-hint key; the hint line is absorbed into the description string.

Workaround

Declare the hint explicitly in the preset command frontmatter (argument-hint: "Describe the feature you want to specify"). It is then carried structurally through apply_argument_hint before serialization, and inject_argument_hint skips injection (key already present).

Specify CLI Version

0.16.2

AI Agent

Claude Code

Operating System

macOS 15 (Apple Silicon, Darwin 25.5.0)

Python Version

3.14

Additional Context

Activity

  1. github-actions commented on Aug 11, 2026

    @github-actions
    Contributor

    Bug assessment — claude-argument-hint-fold: Valid · severity high


    Bug Assessment: Claude ARGUMENT_HINTS fallback injects into folded description scalar

    Report (summarized)

    Issue #3991 (Claude argument-hint corruption in wrap presets) was closed by PR #3996, but the fix is a no-op for the exact configuration that triggered the original report. Two conditions combine:

    1. Bundled core command templates carry no argument-hint frontmatter — hints live in the hardcoded ARGUMENT_HINTS dict in the Claude integration. So the PR [bug-fix] Fix preset-wrap-drops-argument-hint: inherit argument-hint from core template #3996 inheritance-allowlist fix has nothing to inherit, and argument-hint is absent from the frontmatter dict when it reaches serialization.
    2. The ARGUMENT_HINTS fallback (inject_argument_hint) operates on the already-serialized YAML string by string-scanning for the first line starting with description: and inserting the hint on the next line. When yaml.safe_dump folds a long description across multiple lines, the injected line lands inside the continuation of the folded scalar — producing either invalid YAML (plain scalar) or silent key corruption (quoted scalar).

    Symptom

    inject_argument_hint inserts argument-hint: directly after the description: line in the serialized YAML, but for descriptions long enough to be folded by yaml.safe_dump the injected line falls inside the multi-line scalar, either breaking YAML parsing entirely or silently merging the hint text into the description value.

    Reproduction

    1. Create a fresh project: specify init --here --integration claude --script sh --ignore-agent-tools --force
    2. Create a wrap preset with a description ≥ ~150 characters (enough to trigger YAML folding) and strategy: wrap but no explicit argument-hint.
    3. Apply the preset: specify preset add --dev ./presets-src/demo-wrap
    4. Inspect the generated skill: python3 -c "import yaml; print(yaml.safe_load(open('.claude/skills/speckit-specify/SKILL.md').read().split('---')[1]))"

    Expected: argument-hint is a separate key with the correct value.

    Actual (plain scalar): invalid YAML — yaml.parser.ParserError: expected , but found ''

    Actual (quoted scalar, e.g. description contains #): YAML parses but argument-hint key is absent; the hint line is absorbed into the description string.

    [NEEDS CLARIFICATION: Whether 0.16.2 is the latest release or if a 0.16.3+ exists with any partial mitigation.]

    Suspected Code Paths

    • src/specify_cli/integrations/claude/__init__.py:69–114 (inject_argument_hint) — The post-serialization string-scan insertion logic. It matches only the first description: line but does not account for the fact that yaml.safe_dump may have wrapped that description across multiple continuation lines before the next top-level key.
    • src/specify_cli/integrations/claude/__init__.py:194–215 (post_process_skill_content) — Calls inject_argument_hint after the skill content is already fully serialized; by this point the frontmatter dict is no longer accessible.
    • src/specify_cli/_utils.py:71–78 (dump_frontmatter) — Uses yaml.safe_dump with no width override, so the default 80-character fold width applies; long descriptions fold freely.
    • src/specify_cli/agents.py:458–482 (apply_argument_hint) — The structured path that sets argument-hint in the frontmatter dict before serialization; confirmed to be a no-op for bundled core commands because they carry no argument-hint in their template frontmatter.

    Root Cause Hypothesis

    inject_argument_hint assumes that the serialized description: value fits on a single line. When yaml.safe_dump wraps a long description, the injection point is inside the scalar's continuation, not after it. The fix in PR #3996 addressed the inheritance path (preset frontmatter → composed frontmatter dict) but did not address the fallback path where ARGUMENT_HINTS injects after serialization. Confidence: high — the code paths are clear and the two failure modes are mechanistically explained by the interaction between yaml.safe_dump's folding and single-line string injection.

    Proposed Remediation

    Preferred: Move ARGUMENT_HINTS injection to happen before YAML serialization, at the same structured level as apply_argument_hint. Concretely, in _render_skill (or wherever skill_frontmatter is built for a given template stem), look up the stem in ARGUMENT_HINTS and set skill_frontmatter["argument-hint"] directly. Then dump_frontmatter serializes the key safely as a proper YAML entry. inject_argument_hint (the string-based fallback) can remain for the case where a pre-rendered SKILL.md string arrives without the key, but it should be a last-resort — and ideally inject_argument_hint should be made fold-aware regardless (scan past continuation lines before inserting).

    Alternatives:

    • Make inject_argument_hint fold-aware: After finding description:, advance past all continuation lines (lines that start with whitespace or that are part of a block scalar), then insert after the last continuation line. More surgical but fragile — YAML scalar folding rules are subtle.
    • Force single-line descriptions in dump_frontmatter: Pass width=float("inf") to yaml.safe_dump to disable folding. This prevents the symptom but hides long descriptions awkwardly in generated files.

    Files likely to change:

    • src/specify_cli/integrations/claude/__init__.py — primary fix location; modify _render_skill or _build_skill_fm to inject argument-hint into the frontmatter dict before dump_frontmatter is called, and/or harden inject_argument_hint.
    • tests/integrations/test_integration_claude.py — add regression test composing a core template (e.g. specify) with a description long enough to fold, asserting: (a) the output parses as valid YAML, (b) argument-hint is a top-level key, (c) description contains only the preset's description text.

    Tests to add or update:

    • Regression test: test_inject_argument_hint_folded_description — call inject_argument_hint (or the full preset-compose + install cycle) with a description ≥ 150 characters and verify valid YAML output with a correct argument-hint key.
    • Regression test: test_inject_argument_hint_quoted_folded_description — same but with a #-containing description that forces quoting.
    • Parity test: verify the structured path (apply_argument_hint via frontmatter dict) and the fallback path (inject_argument_hint via string) produce identical YAML for the same hint.

    Risks & Considerations

    • Not data-loss, but correctness regression: generated SKILL.md files may be silently wrong (description absorbs hint text) without any error, and users won't notice until Claude's argument-hint UI feature fails to appear.
    • Scope: affects only the Claude integration and only when post_process_skill_content triggers the ARGUMENT_HINTS fallback — i.e., wrap presets for bundled core commands without explicit argument-hint. First-party direct install and presets that declare argument-hint explicitly are unaffected.
    • The workaround is stable: declaring argument-hint explicitly in the preset frontmatter triggers the structured apply_argument_hint path and completely avoids inject_argument_hint. No user data is at risk.
    • API surface: the fix is internal; no public API or CLI interface changes.

    Open Questions

    • [NEEDS CLARIFICATION: Is there a width parameter already passed anywhere to dump_frontmatter that might partially mitigate folding in some cases?]
    • [NEEDS CLARIFICATION: Does _render_skill have access to the template stem at call time, or does the stem need to be threaded through from the caller?]

    Generated by 🐛 Assess Bug from Labeled Issue for issue #4044 · 107.5 AIC · ⌖ 11 AIC · ⊞ 34.1K · ◷

  2. added a commit that references this issue on Aug 11, 2026
    bd595cf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions