Repository navigation
[Bug]: preset wrap composition drops core argument-hint and leaks its value into description #3991
Description
Activity
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
- Slug: preset-wrap-drops-argument-hint
- Created: 2026-08-05T13:51:57Z
- Source: issue [Bug]: preset wrap composition drops core argument-hint and leaks its value into description #3991
- Verdict: valid
- Severity: medium
Report (summarized)
When a preset command overrides a core command with
strategy: wrapand the preset's frontmatter declares onlydescription(noargument-hint), the generated Claude SKILL.md loses theargument-hintkey. The hint value then appears appended inside thedescriptionfield. The YAML remains syntactically valid, so the corruption is silent. Explicitly repeatingargument-hintin the preset's own frontmatter (theapply_argument_hintpath 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: wrapthat inherits itsargument-hintfrom the wrapped core command instead of declaring it explicitly ends up with noargument-hintkey in the generated SKILL.md, and the hint value is concatenated into thedescriptionfield. Because the resulting YAML still parses without error, the corruption goes undetected until an agent surfaces a mangled or missing description.Reproduction
specify init --here --integration claude --script sh --ignore-agent-tools --force— baseline hasargument-hintin the SKILL.md.- Create a wrap preset whose command frontmatter has only
descriptionandstrategy: wrap(noargument-hint), plus a matchingpreset.yml. specify preset add --dev ./presets-src/<wrap-preset>— inspect.claude/skills/speckit-specify/SKILL.md.- Parse the frontmatter:
argument-hintis absent and its value is embedded indescription.
Suspected Code Paths
src/specify_cli/presets/__init__.py:2743–2748— thestrategy: wrapbranch in_register_skillsinherits onlyscriptsandagent_scriptsfromcore_frontmatter;argument-hintis silently skipped.src/specify_cli/presets/__init__.py:2781—apply_argument_hint(frontmatter, frontmatter_data, integration)readsargument-hintfrom the preset's frontmatter. Because noargument-hintwas 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_contentcallsinject_argument_hintusing theARGUMENT_HINTSmap as a string-injection fallback. For known command stems this still injects the key, but the string-based injection is known to corrupt folded/wrappeddescriptionscalars (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 theyaml.safe_dumpoutput, the injectedargument-hint:line lands inside the folded value, the orphaned continuation line(s) become part ofargument-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_scriptsare merged from the base frontmatter butargument-hintis not.
Root Cause Hypothesis
The wrap-composition layer in
_register_skillswas updated (PR #2978) to carryargument-hintforward when the preset's own frontmatter declares it, via the newapply_argument_hinthelper. However, the companion case — where the preset relies on the core template'sargument-hintand 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-basedinject_argument_hintfallback inpost_process_skill_contentcan partially compensate for commands present in theARGUMENT_HINTSmap, 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 inARGUMENT_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_skillsto includeargument-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_layersat 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-hintnow present infrontmatter, the existingapply_argument_hintcall at line 2781 will carry it intofrontmatter_data(the dict serialized beforedump_frontmatter), bypassing the string-injection path entirely and avoiding any scalar-folding corruption risk.Alternatives:
- Widen to all unknown keys: Merge the full
core_frontmatterintofrontmatterfor 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 targetedargument-hintaddition is lower risk. - Remove string-injection fallback: Remove or guard the
inject_argument_hintcall inpost_process_skill_contentfor keys that were not set viaapply_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 usesstrategy: wrapwith noargument-hintin the preset frontmatter. Assert that the generated SKILL.md contains the correctargument-hintvalue and thatdescriptionequals 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-hintto the inheritance allowlist, which could silently inherit a core value that the preset author intended to omit. This is low-risk becauseargument-hintis purely a UX hint for Claude's slash-command input, not a functional directive; omitting it is almost never intentional when wrapping. - The
_compose_layerschange 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-hintto 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_dumpscalar-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 · ◷
- addedbug-fixTrigger the bug-fix agentic workflowTrigger the bug-fix agentic workflow
on Aug 5, 2026 - added a commit that references this issue
on Aug 5, 2026 Pull request created: #3996
Generated by Fix Bug from Labeled Issue · 991.9 AIC · ⌖ 16.7 AIC · ⊞ 38K
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_skillsand_compose_layersso wrap-strategy presets that omit this key now inherit it from the core template, rather than dropping it and risking its value being concatenated intodescription.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-teststage configured, that workflow will validate the fix after review.Posted on behalf of
@mnriemby 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 · ◷
Bug test — preset-wrap-drops-argument-hint:
⚠️ inconclusive · fix from PR #3996 (branchfix/3991-preset-wrap-drops-argument-hint-6fc1afa14c852556, SHAf19fec970bc38edd09116abdfdf424e58db9afb9)
Bug Test Report: Preset wrap composition drops core argument-hint
- Slug: preset-wrap-drops-argument-hint
- Date: 2026-08-05
- Source issue: [Bug]: preset wrap composition drops core argument-hint and leaks its value into description #3991
- Fix under test: PR [bug-fix] Fix preset-wrap-drops-argument-hint: inherit argument-hint from core template #3996 (head SHA
f19fec970bc38edd09116abdfdf424e58db9afb9) - Test command:
[UNRUNNABLE — see Notes] - Scope: N/A — environment setup failure prevented execution
- Result: inconclusive
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
pytestorspecify_clipackage 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:
pytestrequires installation from PyPI. All pip install attempts returned403 Forbiddenfrom the runner's outbound firewall. No cached wheels, no pre-installed pytest, no virtualenv with dependencies was present in the runner. The test commandpython3 -m pytest tests/test_presets.py -k test_wrap_preset_inherits_argument_hint_from_core -vcould 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:-
_register_skills(~line 2746): The loopfor key in ("scripts", "agent_scripts"):is extended tofor key in ("scripts", "agent_scripts", "argument-hint"):. This ensures that when a wrap-strategy preset's frontmatter does not declareargument-hint, the value is inherited fromcore_frontmatterinstead of being silently dropped. -
_compose_layers(~line 5728): The identical allowlist inPresetResolver._compose_layersreceives 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 existingapply_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
descriptionandargument-hint. - Constructs a wrap-preset frontmatter with only
description(noargument-hint). - After
_register_skillsprocessing, assertsparsed.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 otherspecify-cliruntime 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 · ◷
- added a commit that references this issue
on Aug 11, 2026
Bug Description
When a preset overrides a core command with
strategy: wrapand its command file's frontmatter declares onlydescription(noargument-hint), the composed skill file loses the core command'sargument-hintkey — and the orphaned value string is concatenated into thedescriptionvalue. The result still parses as YAML, so the corruption is easy to miss, but the skill metadata is wrong (agents relying ondescription/argument-hintget a mangled description and no argument hint).Steps to Reproduce
Initialize a fresh project:
The core frontmatter contains both keys, e.g.:
Create a minimal wrap preset whose command frontmatter has only
descriptionandstrategy(same layout as in the scaffold example):presets-src/demo-wrap/commands/speckit.specify.md(plus a matching
preset.ymlwithprovides.templates: [{type: command, name: speckit.specify, file: commands/speckit.specify.md, strategy: wrap}])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, anddescriptionshould contain exactly the preset's declared description.Actual Behavior
Parsing the composed frontmatter shows:
argument-hintkey is gone, andDescribe the feature you want to specify) appears inside thedescriptionstring 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 theapply_argument_hintpath 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-hintsurvival, but covering different cases):argument-hintin preset SKILL.md generation when the preset's own frontmatter declares it; the case reported here (preset relies on wrap inheritance from core) is one step outside that fixReading
src/specify_cli/presets/__init__.pyon current main, the wrap-composition layer inheritance appears to carry over onlyscripts/agent_scriptsfrom the base layer, which would explain whyargument-hintis not inherited; the leakage of the value intodescriptionsuggests the frontmatter re-serialization also mis-attaches the orphaned value.