Repository navigation
[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
Activity
github-actions commented
on Aug 11, 2026 on Aug 11, 2026 – with GitHub ActionsContributorMore actionsBug assessment — claude-argument-hint-fold: Valid · severity high
Bug Assessment: Claude ARGUMENT_HINTS fallback injects into folded description scalar
- Slug: claude-argument-hint-fold
- Created: 2026-08-11T12:35:57Z
- Source: issue [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
- Verdict: valid
- Severity: high
Report (summarized)
Issue #3991 (Claude
argument-hintcorruption 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:- Bundled core command templates carry no
argument-hintfrontmatter — hints live in the hardcodedARGUMENT_HINTSdict 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, andargument-hintis absent from the frontmatter dict when it reaches serialization. - The
ARGUMENT_HINTSfallback (inject_argument_hint) operates on the already-serialized YAML string by string-scanning for the first line starting withdescription:and inserting the hint on the next line. Whenyaml.safe_dumpfolds 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_hintinsertsargument-hint:directly after thedescription:line in the serialized YAML, but for descriptions long enough to be folded byyaml.safe_dumpthe injected line falls inside the multi-line scalar, either breaking YAML parsing entirely or silently merging the hint text into the description value.Reproduction
- Create a fresh project:
specify init --here --integration claude --script sh --ignore-agent-tools --force - Create a wrap preset with a
description≥ ~150 characters (enough to trigger YAML folding) andstrategy: wrapbut no explicitargument-hint. - Apply the preset:
specify preset add --dev ./presets-src/demo-wrap - Inspect the generated skill:
python3 -c "import yaml; print(yaml.safe_load(open('.claude/skills/speckit-specify/SKILL.md').read().split('---')[1]))"
Expected:
argument-hintis 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 butargument-hintkey is absent; the hint line is absorbed into thedescriptionstring.[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 firstdescription:line but does not account for the fact thatyaml.safe_dumpmay 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) — Callsinject_argument_hintafter 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) — Usesyaml.safe_dumpwith nowidthoverride, 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 setsargument-hintin the frontmatter dict before serialization; confirmed to be a no-op for bundled core commands because they carry noargument-hintin their template frontmatter.
Root Cause Hypothesis
inject_argument_hintassumes that the serializeddescription:value fits on a single line. Whenyaml.safe_dumpwraps 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 whereARGUMENT_HINTSinjects after serialization. Confidence: high — the code paths are clear and the two failure modes are mechanistically explained by the interaction betweenyaml.safe_dump's folding and single-line string injection.Proposed Remediation
Preferred: Move
ARGUMENT_HINTSinjection to happen before YAML serialization, at the same structured level asapply_argument_hint. Concretely, in_render_skill(or whereverskill_frontmatteris built for a given template stem), look up the stem inARGUMENT_HINTSand setskill_frontmatter["argument-hint"]directly. Thendump_frontmatterserializes 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 ideallyinject_argument_hintshould be made fold-aware regardless (scan past continuation lines before inserting).Alternatives:
- Make
inject_argument_hintfold-aware: After findingdescription:, 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: Passwidth=float("inf")toyaml.safe_dumpto 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_skillor_build_skill_fmto injectargument-hintinto the frontmatter dict beforedump_frontmatteris called, and/or hardeninject_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-hintis a top-level key, (c)descriptioncontains only the preset's description text.
Tests to add or update:
- Regression test:
test_inject_argument_hint_folded_description— callinject_argument_hint(or the full preset-compose + install cycle) with a description ≥ 150 characters and verify valid YAML output with a correctargument-hintkey. - 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_hintvia frontmatter dict) and the fallback path (inject_argument_hintvia string) produce identical YAML for the same hint.
Risks & Considerations
- Not data-loss, but correctness regression: generated
SKILL.mdfiles 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_contenttriggers theARGUMENT_HINTSfallback — i.e., wrap presets for bundled core commands without explicitargument-hint. First-party direct install and presets that declareargument-hintexplicitly are unaffected. - The workaround is stable: declaring
argument-hintexplicitly in the preset frontmatter triggers the structuredapply_argument_hintpath and completely avoidsinject_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
widthparameter already passed anywhere todump_frontmatterthat might partially mitigate folding in some cases?] - [NEEDS CLARIFICATION: Does
_render_skillhave 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 · ◷
- added a commit that references this issue
on Aug 11, 2026 - added a commit that references this issue
on Aug 11, 2026
Bug Description
#3991 (preset wrap composition drops core
argument-hintand leaks its value intodescription) was closed by #3996, but on 0.16.2 the corruption still reproduces for the configuration that motivated the original report: Claude integration + astrategy: wrappreset whosedescriptionis long enough for the YAML dumper to fold it across multiple lines.Two facts combine:
argument-hintto the wrap-composition inheritance allowlist (copied fromcore_frontmatterwhen the preset frontmatter doesn't declare it). But the bundled core command templates (e.g.core_pack/commands/specify.md) declare noargument-hintfrontmatter at all — for Claude the hints live in the hardcodedARGUMENT_HINTSdict insrc/specify_cli/integrations/claude/__init__.py. The docstring ofagents.py::apply_argument_hintstates this explicitly: "Built-in templates carry noargument-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.ARGUMENT_HINTSfallback then injects by raw string insertion into already-serialized YAML.ClaudeIntegration.post_process_skill_content→inject_argument_hintinsertsargument-hint: "…"directly after the first line starting withdescription:. 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 thatdump_frontmatterfolds 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):
yaml.parser.ParserError: expected <block end>, but found '<scalar>').#, forcing single quotes) → the YAML stays valid but theargument-hintkey does not exist and its line is absorbed into thedescriptionstring — the same silent corruption as [Bug]: preset wrap composition drops core argument-hint and leaks its value into description #3991.Steps to Reproduce
Fresh project on 0.16.2:
Create a wrap preset whose command frontmatter declares only a long
description(~150+ chars, so it folds when dumped) andstrategy: wrap— noargument-hint:presets-src/demo-wrap/commands/speckit.specify.mdplus 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 -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
descriptionexactly as declared, plusargument-hint: Describe the feature you want to specifyas its own key (from the ClaudeARGUMENT_HINTSmapping).Actual Behavior
With the description above (plain scalar), the composed frontmatter is invalid YAML:
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:Parsed result: no
argument-hintkey; the hint line is absorbed into thedescriptionstring.Workaround
Declare the hint explicitly in the preset command frontmatter (
argument-hint: "Describe the feature you want to specify"). It is then carried structurally throughapply_argument_hintbefore serialization, andinject_argument_hintskips 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
ARGUMENT_HINTSfallback rather than the inheritance path it was meant to guard.ARGUMENT_HINTSfallback add the key to the frontmatter dict before YAML serialization (the same structured path asapply_argument_hint), instead of post-serialization line insertion — or makeinject_argument_hintfold-aware. A regression test should compose against the actual bundled core template with a fold-length description.