Repository navigation
Description
Summary
PR #1832 (issue #1824) adds a shell input security contract to workflows/squad.md, authored by RETRO. It defines the mandatory channel for attacker-controlled GitHub event text and names four forbidden anti-patterns.
The contract is declared but not enforced. This issue tracks building the gate.
Deliberately scoped out of #1832: that PR is a regression rework under an active Diff Size Guard, and a new gate over compiled gh-aw YAML is materially larger. Filing it separately so the requirement is tracked rather than aspirational.
The contract being enforced
Mandatory channel: attacker-controlled GitHub event text (issue bodies, issue-comment bodies, issue titles, PR titles/bodies) MUST reach the shell only through named step/job env: variables, read via quoted shell parameter expansion.
Forbidden anti-patterns — deliberately named so they are greppable:
| Token | Rule |
|---|---|
UNTRUSTED_TEMPLATE_IN_RUN |
No ${{ github.event.* }} (or expressions derived from it) directly inside a run: block. Unsafe even inside shell quotes — Actions template expansion happens before the shell starts, so shell quoting cannot save you. |
UNTRUSTED_COMMAND_STRING |
No building shell syntax from attacker text: no eval, source, generated script text, or bash -c / sh -c command strings containing the body. |
UNTRUSTED_PRINTF_FORMAT |
Body must never be printf's first argument. Format slot must be a literal ('%s\n'); body belongs in an argument slot. |
UNTRUSTED_AWK_PROGRAM_OR_VAR |
No interpolating attacker text into an awk program string, and no raw body through awk -v — awk -v performs escape-sequence processing on values and can mutate parser input. Body reaches awk on stdin, program stays static and single-quoted. |
Full contract with per-hop requirements: workflows/squad.md, section Shell input security contract [MANDATORY] (added in #1832).
Why the gate must read compiled output, not markdown
This is the crux. gh-aw compiles markdown into workflow YAML. Rule 1 — "the compiled workflow must not contain attacker-controlled ${{ github.event... }} expressions in any run: block" — is a property of the compiled artifact. A markdown-only linter cannot observe it.
RETRO stated the residual risk plainly during authoring:
The real gh-aw compiled output is absent from this repo, so compiler-channel compliance remains unmeasured.
So the gate has a prerequisite: compiled gh-aw output must be available to CI (committed lock, build step, or equivalent). Establishing that is part of this issue. If it turns out compiled output genuinely cannot be produced in CI, say so and downgrade the contract's verification paragraph to match reality rather than leaving a requirement nothing can check.
Acceptance criteria
-
The gate inspects compiled gh-aw workflow YAML, not just
workflows/*.md. If compiled output is unavailable, the gate fails loudly rather than passing vacuously. -
It fails when any compiled
run:block contains attacker-controlled GitHub event expressions. -
It fails when parser code passes a body variable as a
printfformat, intoeval/bash -c, or into anawkprogram orawk -v. -
Positive-control fixture, required. RETRO's wording, and it is the acceptance bar:
A gate that cannot turn red on a fixture containing
run: printf '%s\n' "${{ github.event.issue.body }}"is not a valid gate.The fixture must live in the repo and be exercised by the gate's own test, so the gate is proven capable of failing rather than assumed to be.
-
The failure message names the violated anti-pattern token and the offending file and line. A status-only failure is insufficient — per fix(gh-aw): fail loudly when /squad parses no command (#1824) #1832's mutation testing, a generic-shaped diagnostic passes a status-only assertion clean.
-
The remediation text the gate prints must be a command that can actually observe the violation. Per
.squad/decisions.md: the instructions a gate prints are part of the gate.
Related
Same defect cluster as #1824, #1812, #1801, #1822, #1827, #1833 — gates that structurally cannot observe the failure they exist to catch. Standing decision (.squad/decisions.md, 2026-08-20): a permanently green — or permanently red — gate is equivalent to no gate.
Contract authored by RETRO during #1832 review. Injection finding originally raised by the automated PR reviewer on #1832 and adjudicated NON-BLOCKING by FIDO after measuring 8 payloads through the live pipeline with 0 side effects — the runtime pipeline is safe today; this gate exists to keep it that way.
Activity
fix(gh-aw): normalize bare workflow_dispatch commands before PC-1
fix(gh-aw): fail loudly when /squad parses no command (#1824) (#1832)
chore(squad): gh-aw wave-1 session records + repair 3 lost history ar…
bradygaster commentedon Aug 24, 2026
Design update from #1842 / PR #1849 — this issue is no longer budget-constrained.
The premise that the shell-input security contract (
workflows/squad.mdL245-299, 2,695 bytes) had to be deleted came from the prompt-budget gate reporting ~26 bytes of headroom. That gate was wrong on both sides:102400isdefaultRepoMemoryMaxFileSizein gh-awpkg/workflow/repo_memory.go— a repo-memory file cap, unrelated to prompts.## skill:block at setup/interpolation (gh-aw.github/aw/skills.md). ~65 KB of the 97 KB it measured never enters the initial request. Actual ambient prompt: 31.8 KB.Real headroom is ~70 KB, not 26 bytes.
What changes here: the security contract can move into a
## skill:block rather than being removed. It stays authored, versioned, and loadable on demand — the agent pulls it when it is about to construct shell input, which is arguably better placement than ambient anyway.And #1834 is no longer a prerequisite for #1842 — that dependency existed only to reclaim bytes.
One constraint if you do convert it to a skill block: the section body must not contain
##headings unless you add an explicit## end skill: `name`marker. gh-aw closes an unterminated block at the next H2 and silently discards everything between that boundary and the next skill marker. Thediscards no content during extractiontest intest/gh-aw-quality.test.tsguards this, but it is worth knowing before you write it.bradygaster commentedon Aug 24, 2026
Prerequisite finding: compiled output is unavailable to CI today, and the existing compile gate silently skips
Investigated the prerequisite this issue calls out ("compiled gh-aw output must be available to CI ... Establishing that is part of this issue"). Measured, not assumed:
1. A
gh aw compilegate already exists — and it is a no-op in CI.test/gh-aw-quality.test.ts:1092:No CI job installs the
gh awextension (grep of.github/workflows/forgh aw/gh-aw/extension installmatches onlyagentics-maintenance.yml, which is a generated agentic workflow, not a test job). SoghAwAvailableisfalseon every CI run and the test skips rather than fails — a vacuous pass.That is this issue's own defect class, already present in the repo: a gate that structurally cannot observe the failure it exists to catch.
2. No
.lock.ymlis committed here. Sources live inworkflows/; locks are compiled in the consumer's repo. So there is no compiled artifact on disk for a gate to read either.Consequence for scope: acceptance criterion 1 ("the gate inspects compiled gh-aw workflow YAML ... If compiled output is unavailable, the gate fails loudly rather than passing vacuously") cannot be met by a linter alone. It requires new CI infrastructure — install the
gh awextension, compileworkflows/*into a scratch workspace, and treat absence of compiled output as failure.High-leverage note: that one enabling step is shared work. It simultaneously (a) activates the existing skipped compile gate above, (b) unblocks this issue's contract gate, and (c) satisfies the follow-up promised in PR #1854 (#1827) — linting the emitted
.lock.ymlfiles, which today are never linted by anything because they are never produced in CI.Deferred by owner decision this session to prioritize the E2E milestone verdict (M2). Filing this comment so the prerequisite is recorded rather than rediscovered.
fix(gh-aw): close three detector bypasses in the shell contract gate
test(gh-aw): enforce shell input security contract over compiled outp…
fix(gh-aw): bound activation label capacity and make truncation expli…
fix(gh-aw): restore public installability after #1977 (#1979)
chore(release): promote dev to main (v1.0.0) (#2154)