Skip to content

[Bug]: preset wrap composition drops core argument-hint and leaks its value into description #3991

Description

@takaya0

Bug Description

When a preset overrides a core command with strategy: wrap and its command file's frontmatter declares only description (no argument-hint), the composed skill file loses the core command's argument-hint key — and the orphaned value string is concatenated into the description value. The result still parses as YAML, so the corruption is easy to miss, but the skill metadata is wrong (agents relying on description/argument-hint get a mangled description and no argument hint).

Steps to Reproduce

  1. Initialize a fresh project:

    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
    head -12 .claude/skills/speckit-specify/SKILL.md

    The core frontmatter contains both keys, e.g.:

    description: Create or update the feature specification from a natural language feature description.
    argument-hint: Describe the feature you want to specify
  2. Create a minimal wrap preset whose command frontmatter has only description and strategy (same layout as in the scaffold example):

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

    ---
    description: "Wrapped speckit.specify"
    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 -12 .claude/skills/speckit-specify/SKILL.md
    python3 -c "import yaml,io; f=open('.claude/skills/speckit-specify/SKILL.md'); t=f.read().split('---')[1]; print(yaml.safe_load(t))"

Expected Behavior

Core frontmatter keys that the preset does not explicitly override (such as argument-hint) should be inherited into the composed skill frontmatter, and description should contain exactly the preset's declared description.

Actual Behavior

Parsing the composed frontmatter shows:

  • the argument-hint key is gone, and
  • its value (Describe the feature you want to specify) appears inside the description string appended to the preset's description.

Because the YAML remains syntactically valid, nothing fails loudly; we only noticed because the agent's skill list started showing a fallback title instead of the intended description.

Workaround: explicitly repeating the core value in the preset command frontmatter (argument-hint: "Describe the feature you want to specify") produces a correct composition (this goes through the apply_argument_hint path added in #2978).

Specify CLI Version

0.15.2

AI Agent

Claude Code

Operating System

macOS 15 (Apple Silicon, Darwin 25.5.0)

Python Version

3.14.6

Additional Context

Related earlier work (all about argument-hint survival, but covering different cases):

Reading src/specify_cli/presets/__init__.py on current main, the wrap-composition layer inheritance appears to carry over only scripts / agent_scripts from the base layer, which would explain why argument-hint is not inherited; the leakage of the value into description suggests the frontmatter re-serialization also mis-attaches the orphaned value.

Activity

  1. github-actions commented on Aug 5, 2026

    @github-actions
    Contributor

    Bug assessment — preset-wrap-drops-argument-hint: Valid · severity medium


    Bug Assessment: Preset wrap composition drops core argument-hint and leaks its value into description

    Report (summarized)

    When a preset command overrides a core command with strategy: wrap and the preset's frontmatter declares only description (no argument-hint), the generated Claude SKILL.md loses the argument-hint key. The hint value then appears appended inside the description field. The YAML remains syntactically valid, so the corruption is silent. Explicitly repeating argument-hint in the preset's own frontmatter (the apply_argument_hint path from PR #2978) works around the issue.

    Reporter: takaya0 · CLI version 0.15.2 · Claude Code · macOS 15 / Python 3.14.6

    Related issues: #2903, #2916 (extension path fix), PR #2978 (preset explicit-declaration fix), #3391 (different frontmatter-corruption family).

    Symptom

    A preset using strategy: wrap that inherits its argument-hint from the wrapped core command instead of declaring it explicitly ends up with no argument-hint key in the generated SKILL.md, and the hint value is concatenated into the description field. Because the resulting YAML still parses without error, the corruption goes undetected until an agent surfaces a mangled or missing description.

    Reproduction

    1. specify init --here --integration claude --script sh --ignore-agent-tools --force — baseline has argument-hint in the SKILL.md.
    2. Create a wrap preset whose command frontmatter has only description and strategy: wrap (no argument-hint), plus a matching preset.yml.
    3. specify preset add --dev ./presets-src/<wrap-preset> — inspect .claude/skills/speckit-specify/SKILL.md.
    4. Parse the frontmatter: argument-hint is absent and its value is embedded in description.

    Suspected Code Paths

    • src/specify_cli/presets/__init__.py:2743–2748 — the strategy: wrap branch in _register_skills inherits only scripts and agent_scripts from core_frontmatter; argument-hint is silently skipped.
    • src/specify_cli/presets/__init__.py:2781 — apply_argument_hint(frontmatter, frontmatter_data, integration) reads argument-hint from the preset's frontmatter. Because no argument-hint was declared there and none was inherited in step above, this call is a no-op.
    • src/specify_cli/integrations/claude/__init__.py:206–210 — post_process_skill_content calls inject_argument_hint using the ARGUMENT_HINTS map as a string-injection fallback. For known command stems this still injects the key, but the string-based injection is known to corrupt folded/wrapped description scalars (the exact mechanism PR fix(presets): preserve argument-hint in preset SKILL.md generation #2978 addressed for the dict-based path). When the description YAML scalar folds across multiple lines in the yaml.safe_dump output, the injected argument-hint: line lands inside the folded value, the orphaned continuation line(s) become part of argument-hint's value, and the description is correspondingly truncated or mangled.
    • src/specify_cli/presets/__init__.py:5726–5730 (_compose_layers) — analogous inheritance gap: scripts/agent_scripts are merged from the base frontmatter but argument-hint is not.

    Root Cause Hypothesis

    The wrap-composition layer in _register_skills was updated (PR #2978) to carry argument-hint forward when the preset's own frontmatter declares it, via the new apply_argument_hint helper. However, the companion case — where the preset relies on the core template's argument-hint and expects it to be inherited via wrap composition — was not addressed. The explicit allowlist at lines 2746–2748 ("scripts", "agent_scripts") does not include "argument-hint", so the core value is discarded. The string-based inject_argument_hint fallback in post_process_skill_content can partially compensate for commands present in the ARGUMENT_HINTS map, but it is susceptible to YAML scalar folding corruption — which is precisely why PR #2978 introduced the dict-based path. For any extension-provided command not in ARGUMENT_HINTS, the fallback provides nothing at all.

    Confidence: high. The code clearly performs the inheritance gap, the reporter's workaround confirms the dict-based path works correctly, and the corruption mechanism (string injection into a folded scalar) is already documented in the codebase.

    Proposed Remediation

    Preferred: Extend the inheritance allowlist in _register_skills to include argument-hint:

    # src/specify_cli/presets/__init__.py  ~line 2746
    for key in ("scripts", "agent_scripts", "argument-hint"):
        if key not in frontmatter and key in core_frontmatter:
            frontmatter[key] = core_frontmatter[key]
    

    Apply the same extension to _compose_layers at lines 5726–5730, which performs an identical inheritance step for the composed-file path:

    for key in ("scripts", "agent_scripts", "argument-hint"):
        if key not in top_fm and key in base_fm:
            top_fm[key] = base_fm[key]
    

    With argument-hint now present in frontmatter, the existing apply_argument_hint call at line 2781 will carry it into frontmatter_data (the dict serialized before dump_frontmatter), bypassing the string-injection path entirely and avoiding any scalar-folding corruption risk.

    Alternatives:

    • Widen to all unknown keys: Merge the full core_frontmatter into frontmatter for keys not present in the preset, filtering only the composition directives (strategy). Safer long-term but risks inheriting keys the preset intentionally does not want. The targeted argument-hint addition is lower risk.
    • Remove string-injection fallback: Remove or guard the inject_argument_hint call in post_process_skill_content for keys that were not set via apply_argument_hint. This eliminates the corruption risk entirely but makes any gap in the dict-based path immediately visible as a missing key rather than a corrupted value. Could be a follow-up hardening step after the primary fix.

    Files likely to change:

    • src/specify_cli/presets/__init__.py (lines ~2746 and ~5728)
    • tests/test_presets.py (new test in the existing argument-hint test class)

    Tests to add or update:

    • Add a test analogous to test_argument_hint_preserved_for_preset_command (line 4695) that uses strategy: wrap with no argument-hint in the preset frontmatter. Assert that the generated SKILL.md contains the correct argument-hint value and that description equals exactly the preset's declared description (not a concatenation).
    • Add a parallel test where the wrapped command is an extension-provided command rather than a core command, to cover the ARGUMENT_HINTS-map gap.

    Risks & Considerations

    • The fix adds argument-hint to the inheritance allowlist, which could silently inherit a core value that the preset author intended to omit. This is low-risk because argument-hint is purely a UX hint for Claude's slash-command input, not a functional directive; omitting it is almost never intentional when wrapping.
    • The _compose_layers change affects the composed-file path used by non-skills agents; verify the integration tests for those agents still pass.
    • No API breakage: the SKILL.md file format is consumed by Claude Code, not by other parts of the CLI. Adding argument-hint to a SKILL.md is additive.

    Open Questions

    • The reporter uses Python 3.14.6, which is a pre-release build. Confirm whether the yaml.safe_dump scalar-folding behaviour in CPython 3.14 differs from 3.12/3.13 and whether that affects the exact reproduction steps — the root cause (missing inheritance) is version-independent, but the exact corruption shape may vary.

    Generated by 🐛 Assess Bug from Labeled Issue for issue #3991 · 967.9 AIC · ⌖ 11.1 AIC · ⊞ 34.1K · ◷

  2. added
    bug-fixTrigger the bug-fix agentic workflow
    on Aug 5, 2026
  3. github-actions commented on Aug 5, 2026

    @github-actions
    Contributor

    Pull request created: #3996

    Generated by Fix Bug from Labeled Issue · 991.9 AIC · ⌖ 16.7 AIC · ⊞ 38K

  4. github-actions commented on Aug 5, 2026

    @github-actions
    Contributor

    A draft PR has been opened to address this bug: preset-wrap-drops-argument-hint — added "argument-hint" to the wrap-composition inheritance allowlist in _register_skills and _compose_layers so wrap-strategy presets that omit this key now inherit it from the core template, rather than dropping it and risking its value being concatenated into description.

    A regression test (test_wrap_preset_inherits_argument_hint_from_core) was also added.

    Next step: a maintainer should review the draft PR, and if the repo has the bug-test stage configured, that workflow will validate the fix after review.

    Posted on behalf of @mnriem by GitHub Copilot (model: claude-sonnet-4.6, autonomous)

    Generated by 🛠️ Fix Bug from Labeled Issue for issue #3991 · 991.9 AIC · ⌖ 16.7 AIC · ⊞ 38K · ◷

  5. github-actions commented on Aug 5, 2026

    @github-actions
    Contributor

    Bug test — preset-wrap-drops-argument-hint: ⚠️ inconclusive · fix from PR #3996 (branch fix/3991-preset-wrap-drops-argument-hint-6fc1afa14c852556, SHA f19fec970bc38edd09116abdfdf424e58db9afb9)


    Bug Test Report: Preset wrap composition drops core argument-hint

    Summary

    The fix in PR #3996 was successfully checked out. However, the test suite could not be executed because PyPI package installation is blocked by the runner's network firewall (HTTP 403), and no pre-installed pytest or specify_cli package was available. The test report is therefore inconclusive: the tests themselves could not be run, but the fix and new regression test were verified by code inspection (see Notes).

    Test Results

    Metric Count
    Passed N/A
    Failed N/A
    Skipped N/A
    Errored N/A
    Duration N/A

    Failures (if any)

    None — tests did not run.

    Verification vs. Historical Fix

    Not applicable (no separate historical fix referenced; the fix under test is the proposed resolution for this open bug).

    Notes & Caveats

    Why inconclusive: pytest requires installation from PyPI. All pip install attempts returned 403 Forbidden from the runner's outbound firewall. No cached wheels, no pre-installed pytest, no virtualenv with dependencies was present in the runner. The test command python3 -m pytest tests/test_presets.py -k test_wrap_preset_inherits_argument_hint_from_core -v could not be executed.

    Code-inspection verification of the fix (PR #3996):

    The diff is minimal and targeted — exactly two one-line changes in src/specify_cli/presets/__init__.py:

    1. _register_skills (~line 2746): The loop for key in ("scripts", "agent_scripts"): is extended to for key in ("scripts", "agent_scripts", "argument-hint"):. This ensures that when a wrap-strategy preset's frontmatter does not declare argument-hint, the value is inherited from core_frontmatter instead of being silently dropped.

    2. _compose_layers (~line 5728): The identical allowlist in PresetResolver._compose_layers receives the same addition, covering the composed-file path used by non-skills agents.

    Both changes correctly feed the inherited value into frontmatter, which is then consumed by the existing apply_argument_hint(frontmatter, frontmatter_data, integration) call at line 2781 — the same dict-based path that PR #2978 established as correct for the explicit-declaration case. This fully matches the assessment's preferred remediation.

    New regression test (tests/test_presets.py):

    A new test test_wrap_preset_inherits_argument_hint_from_core (76 lines) was added. Inspection confirms it:

    • Constructs a core template frontmatter with both description and argument-hint.
    • Constructs a wrap-preset frontmatter with only description (no argument-hint).
    • After _register_skills processing, asserts parsed.get("argument-hint") == core_arg_hint.
    • Asserts parsed["description"] == preset_description (no concatenation of the hint value).

    This directly targets the regression described in issue #3991.

    What would unblock a real test run: The runner would need outbound access to PyPI (or a local package mirror with pytest, pytest-cov, pyyaml, typer, click, rich, and other specify-cli runtime deps), or a pre-built wheel cache / virtualenv baked into the runner image.

    Unverified

    None.

    Generated by 🧪 Test a Bug Fix from a Labeled Issue for issue #3991 · 571.8 AIC · ⌖ 11 AIC · ⊞ 38.4K · ◷

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