Skip to content

fix: strip scripts: from extension command frontmatter in command mode - #4559

Open
chelsealong wants to merge 2 commits into
github:mainfrom
chelsealong:fix/4554-extension-scripts-frontmatter-leak
Open

fix: strip scripts: from extension command frontmatter in command mode#4559
chelsealong wants to merge 2 commits into
github:mainfrom
chelsealong:fix/4554-extension-scripts-frontmatter-leak

Conversation

@chelsealong

Copy link
Copy Markdown
Contributor

Description

Fixes #4554.

CommandRegistrar.register_commands() renders extension commands for
non-SKILL.md agents by consuming the source scripts: frontmatter
block for {SCRIPT} substitution via resolve_skill_placeholders().
In the markdown branch (e.g. Copilot's .github/agents/*.agent.md
commands-mode layout), the now-consumed key was never removed from the
frontmatter dict before render_markdown_command() serialized the
full dict as the final file's frontmatter. IntegrationBase.process_template()
— the core template render path — already strips scripts: as an
explicit step, so the two render paths disagreed and a build-time key
leaked into agent-visible output, exactly as described in the issue.

The fix pops scripts from frontmatter right after the
resolve_skill_placeholders() call in the markdown branch of
register_commands(), once it's no longer needed. The pop is scoped
to extension_id is not None (i.e. extension-owned commands only)
because register_commands() is also shared by the preset system,
where an existing test (test_register_commands_inherits_scripts_from_core)
intentionally asserts that a strategy: wrap preset command retains
the merged scripts: key in this same render step. That's a separate,
pre-existing behavior outside the scope of this issue, so it's left
untouched.

The toml and yaml branches are not affected by this leak and need
no pop: render_toml_command() only ever reads the description key
off frontmatter, and render_yaml_command() only ever reads
title/description — neither serializes the full frontmatter dict,
so there is nothing for a scripts: key to leak into. (An earlier
version of this PR added the same pop to those two branches as
a defensive copy-paste; removed after confirming with a standalone
repro that it was dead code — neither branch's output ever contains a
scripts: key, fix or no fix.)

Skills-mode extension rendering (_register_extension_skills /
register_skill_command) was already unaffected — those paths build a
fresh SKILL.md frontmatter dict from scratch and never copy scripts:
into it — so no change was needed there.

Testing

Added test_command_mode_registration_strips_scripts_key in
tests/test_extensions.py, which installs an extension command
declaring scripts: and registers it for the copilot agent (command
mode, .agent.md output) — mirroring the issue's reproduction.

Confirmed the test fails without the fix:

$ git checkout HEAD~1 -- src/specify_cli/agents.py  # (before the fix)
$ uv run pytest tests/test_extensions.py -k test_command_mode_registration_strips_scripts_key -q
FAILED tests/test_extensions.py::TestCommandRegistrar::test_command_mode_registration_strips_scripts_key
  assert 'scripts:' not in '---\ndescri..."$ARGUMENTS"'
  'scripts:' is contained here:
    ---
    description: Scripted command
    scripts:
      sh: .specify/scripts/bash/setup-plan.sh --json "{ARGS}"
      ps: .specify/scripts/powershell/setup-plan.ps1 -Json
    ---

And passes with the fix applied:

$ uv run pytest tests/test_extensions.py -k test_command_mode_registration_strips_scripts_key -q
1 passed in 0.13s

Full test_extensions.py + test_presets.py suite (unmodified elsewhere):

$ uv run pytest tests/test_extensions.py tests/test_presets.py -q
1214 passed in 14.82s

AI Disclosure

This PR was written primarily by an autonomous AI coding agent (Claude
Code, model Claude Sonnet 5), including the investigation, the code
change, and the regression test. I reviewed the diff and the test
output before opening this PR.

CommandRegistrar.register_commands() renders extension commands for
non-SKILL.md agents (markdown/toml/yaml formats) by consuming the
scripts: block for {SCRIPT} substitution via resolve_skill_placeholders(),
but never removed the now-unused key from the frontmatter dict before
serializing the final output. IntegrationBase.process_template() (the
core template path) already strips scripts: as its own step 3, so core
and extension renders disagreed and a build-time key leaked into
agent-facing files (e.g. .github/agents/*.agent.md).

Pop scripts: from the frontmatter after it has been consumed, scoped to
extension-owned commands (extension_id set) so preset command rendering,
which intentionally keeps scripts: at this layer for the wrap-strategy
composition tested in test_register_commands_inherits_scripts_from_core,
is unaffected.

Fixes github#4554

Assisted-by: Claude Code (model: claude-sonnet-5, autonomous)
The toml and yaml branches of register_commands() popped scripts:
from frontmatter defensively, copying the pattern from the markdown
branch. But render_toml_command() only ever reads the description
key off frontmatter, and render_yaml_command() only ever reads
title/description — neither serializes the full frontmatter dict, so
scripts: can never leak into their output. Confirmed via a standalone
repro that both branches produce identical output with or without the
pop. Keep only the markdown-branch pop, which is the one that fixes
the actual leak described in github#4554.

Assisted-by: Claude Code (model: claude-sonnet-5, autonomous)
@chelsealong
chelsealong requested a review from mnriem as a code owner September 12, 2026 11:24
@mnriem mnriem added the triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extension command rendering does not strip scripts: from the agent-facing frontmatter

2 participants