Back to Ruflo

ADR-127 — GitHub Skills, Agents, and Init-Template Modernization: CI Guards, Supply-Chain Review, and Provenance

v3/docs/adr/ADR-127-github-stack-modernization.md

3.10.018.5 KB
Original Source

ADR-127 — GitHub Skills, Agents, and Init-Template Modernization: CI Guards, Supply-Chain Review, and Provenance

Status: Proposed (2026-05-21) Date: 2026-05-21 Authors: claude (drafted with rUv) Related: ADR-102 (plugin-hook-CLI flag regression CI guard), ADR-103 (witness temporal history), ADR-118 (AIDefence 2.3.0), ADR-125 (memory consolidation), ADR-126 (neural-trader substrate), ruflo issues #2017, #1922, #2078, #2086 Supersedes: nothing — extends the CI smoke pattern established by ADR-102 to the .github skills/agents surface

Context

The .github-related surface in ruflo ships in two layers:

  1. Dogfood layer.claude/skills/github-*/SKILL.md (5 skills: code-review, multi-repo, project-management, release-management, workflow-automation), .claude/agents/github/*.md (13 agents), .claude/helpers/github-{safe.js,setup.sh}. These drive our own workflow daily.

  2. Init-template layerv3/@claude-flow/cli/.claude/commands/github/*.md (19 command files), v3/@claude-flow/cli/.claude/helpers/github-{safe.js,setup.sh}. These are materialized verbatim into every user project by ruflo init.

Both layers were last substantively updated on 2025-10-19 — seven months before this ADR. In the same period the project shipped ~10 targeted CI guard + supply-chain hardening PRs: #1922 (CWE-347 plugin registry signature), #2017 (pre-bash hook silent exit 0), #2046 (5-layer supply-chain audit), #2060 (Ed25519 CWE-347 pattern formalization), #2079 (attribution opt-in), #2086 (ruvllm WASM auto-init). None of those fixes touched the .github surface.

A read-only review of both layers reveals four categories of gap:

Category A: Third-party Actions are mutable floating refs

Every workflow snippet in the skills and init-template commands uses actions/checkout@v3 and actions/setup-node@v3. Both are mutable floating tags. scripts/audit-supply-chain.mjs (introduced in #2046, ADR-102 supply-chain hardening) tracks CVE + lockfile + allowlist + typosquat across npm packages but has zero coverage of the GitHub Actions surface. The github-project-management skill also recommends ruvnet/swarm-action@v1 (skill line 976) — a first-party action that is also unpinned. The existing smoke-plugin-registry-signature.mjs proves that static-contract checks on text files are fast and reliably catch regressions; the same pattern applied to uses: lines would require under 30 lines of JavaScript.

Category B: Unquoted GitHub event fields in swarm-pr.md / swarm-issue.md

.claude/agents/github/swarm-pr.md line 57 and its init-template copy interpolate ${{ github.event.comment.body }} directly into a shell if [[ ... ]] test and a subsequent --comment "${{ ... }}" argument. Any PR comment containing shell metacharacters (\...`, $(...), ;) would expand under that interpolation in a pull_requestorissue_commenttriggered workflow. Thegithub-project-managementskill has a correct "Security Considerations" section (skill lines 17-25) warning explicitly against this pattern, but the two agents implementing the warning do not follow it.github-safe.js (.claude/helpers/github-safe.js) already demonstrates the correct mitigation: write the body to a temp file and pass --body-file`. The gap is that the agents don't route through that helper.

The class of failure — silent success when the check was never run — matches #2017 exactly: the pre-bash hook exited 0 on dangerous payloads not because the block was absent but because a TypeError was silently swallowed. Any future refactor of the GitHub agents faces the same swallow risk unless there is a behavioral smoke.

Category C: No tools: restrictions on 12 of 13 agents

The github-project-management skill is the single artifact with allowed-tools: in its frontmatter. All other skills and all 13 agents omit the field entirely, meaning they run with the full tool namespace when materialized. This includes WebFetch, which can be pointed at any URL — including URLs embedded in issue bodies or PR descriptions.

The fix for #2017 added behavioral checks that drive real input payloads against both the dogfood and published-template copies of hook-handler.cjs. No analogous check exists for github-safe.js.

Category D: Attribution footers are not gated on --attribution opt-in

v3/@claude-flow/cli/src/init/settings-generator.ts lines 55-60 (introduced in #2079) implement opt-in Co-Authored-By using a no-reply bot email. The 19 GitHub command files embed hardcoded "🤖 Generated with Claude Code" footers in PR/issue body templates, unconditionally. A user who runs ruflo init without --attribution still gets attribution injected into every PR or issue the GitHub agents post on their behalf.

Decision

Land a four-phase modernization of the .github skills/agents/helpers/init-template surface, following the same deliver-a-runnable-artifact-per-phase discipline as ADR-125 and ADR-126.

Phase 1 — Static-contract smokes and supply-chain extension (small; ships first)

scripts/smoke-github-safe-injection.mjs (new)

Generalizes smoke-pre-bash-hook.mjs (introduced for #2017) to the GitHub helper surface. Drives adversarial bodies through github-safe.js and asserts the body lands in the temp file verbatim, not shell-expanded. Runs against both copies:

  • .claude/helpers/github-safe.js (dogfood)
  • v3/@claude-flow/cli/.claude/helpers/github-safe.js (init template)

Test cases: body containing backticks, body containing $(), body containing semicolons, body longer than 256KB (must be rejected, not truncated), empty body (no-op path, exit 0).

scripts/smoke-github-actions-pins.mjs (new)

Static scan of three trees:

  • .claude/agents/github/*.md
  • .claude/skills/github-*/SKILL.md
  • v3/@claude-flow/cli/.claude/commands/github/*.md

For every uses: line, asserts the ref is either (a) SHA-pinned (uses: owner/repo@<40-hex>) or (b) listed in a new actions key added to .github/supply-chain/allowed-deps.json. First offense: list the violating file + line. Fails on any violation. Zero runtime deps — pure readFileSync + regex.

.github/supply-chain/allowed-deps.json — add:

json
"actions": {
  "allowed": ["actions/checkout", "actions/setup-node", "pnpm/action-setup"],
  "minimumVersion": {"actions/checkout": "v4", "actions/setup-node": "v4"}
}

v3-ci.yml changes

Add two smoke jobs gated by path filters:

yaml
# Phase 1 smokes — gate on .github surface changes
on:
  push:
    paths:
      - '.claude/agents/github/**'
      - '.claude/skills/github-*/**'
      - 'v3/@claude-flow/cli/.claude/commands/github/**'
      - '.claude/helpers/github-safe.js'
      - 'v3/@claude-flow/cli/.claude/helpers/github-safe.js'
      - 'scripts/smoke-github-*.mjs'
      - '.github/supply-chain/allowed-deps.json'

Acceptance: both smokes exit 0 in CI on first commit.

Phase 2 — Helper hardening and agent frontmatter (small)

Generalizes the github-safe.js hardening pattern and the hook-handler.cjs per-copy discipline to the remaining helpers.

github-safe.js (both copies) — add explicit maxBuffer cap (256KB, matching GitHub API body field limit) and reject oversized bodies before writing the temp file. Add a GITHUB_SAFE_VERSION constant so the injection smoke can assert the correct copy is installed. Add set -e equivalent (strict error handling) for the shell fallback path.

github-setup.sh (both copies) — add set -euo pipefail. Replace gh auth status &> /dev/null with a check that parses the output for scope sufficiency. Comment the security rationale.

13 agent frontmatter tools: lines — add explicit tool restrictions. Reference pattern: the code-review-swarm.md agent should list Bash, Read, Grep, Glob, mcp__claude-flow__* and explicitly omit WebFetch, Write. The release-manager.md and release-swarm.md agents may include Write (for CHANGELOG) but not WebFetch. No agent that processes GitHub-hosted content (issue bodies, PR descriptions, label names) should have WebFetch in its tool list.

swarm-pr.md and swarm-issue.md (both dogfood and init-template copies) — replace the unquoted ${{ github.event.comment.body }} and ${{ github.event.label.name }} interpolations with temp-file indirection following the github-safe.js pattern. Specifically:

bash
# Before (vulnerable)
if [[ "${{ github.event.comment.body }}" == /swarm* ]]; then
  npx ruv-swarm github handle-comment --comment "${{ github.event.comment.body }}"

# After
COMMENT_BODY_FILE=$(mktemp)
printf '%s' "${{ github.event.comment.body }}" > "$COMMENT_BODY_FILE"
if grep -q '^/swarm' "$COMMENT_BODY_FILE"; then
  npx ruv-swarm github handle-comment --comment-file "$COMMENT_BODY_FILE"
fi
rm -f "$COMMENT_BODY_FILE"

Ties directly to the github-safe.js temp-file pattern already documented in .claude/helpers/.

Acceptance: smoke-github-safe-injection.mjs passes with the hardened copies; grep -r 'github.event.comment.body.*--comment' .claude/agents/ v3/@claude-flow/cli/.claude/ returns no matches.

Phase 3 — Action pin upgrade and deprecated-action smoke (small)

scripts/smoke-github-release-no-deprecated-action.mjs (new)

Asserts that no file in scope references:

  • actions/create-release@* (archived)
  • softprops/action-gh-release@v1 (mutable floating)

And positively asserts that any workflow snippet containing release in its job name uses gh release create (the gh CLI path) rather than an Actions-based release step.

Upgrade @v3@v4 in all affected files: 5 agent files (release-manager.md, release-swarm.md, swarm-pr.md, workflow-automation.md, repo-architect.md — plus their init-template copies) and both skill files (github-release-management/SKILL.md, github-code-review/SKILL.md). This is a mechanical find-and-replace; the smoke in Phase 1 will catch future regressions.

The ruvnet/swarm-action@v1 reference in github-project-management/SKILL.md (line 976) is either SHA-pinned to the current HEAD of that repo or replaced with the equivalent inline run: step. A SHA pin is preferred — it satisfies the allow-list check without removing the reference.

Acceptance: smoke-github-actions-pins.mjs passes against all files in scope; smoke-github-release-no-deprecated-action.mjs passes.

Phase 4 — Attribution gate and last-updated stamps (small)

Init-template command files — gate the 🤖 Generated with footer on the attribution option that settings-generator.ts already exposes. This is implemented via a new ATTRIBUTION_FOOTER variable in helpers-generator.ts that is set to an empty string by default and to the bot-identity string when options.attribution === true. Each of the 19 command files references the variable rather than hardcoding the footer.

This follows the precedent from settings-generator.ts lines 55-60 (introduced in #2079 / #2078). The bot email [email protected] applies to commit trailers; the PR/issue body footer should use 🤖 Generated with [RuFlo](https://github.com/ruvnet/ruflo) (same PR footer already in settings-generator.ts:60) when attribution is opted in, and no footer otherwise.

Last Updated stamps on the four skill SKILL.md files are updated from 2025-10-19 to the date this ADR ships. A comment in each file notes: "update this date when you change the skill so the smoke can detect stale copies." This is convention, not enforced — no smoke needed.

Acceptance: ruflo init (without --attribution) produces command files with no 🤖 footer string in any PR/issue body template; ruflo init --attribution produces the RuFlo bot-identity footer.

Why this is the right shape of fix

All four phases are connect-the-existing-pieces work:

  • Phase 1 generalizes smoke-pre-bash-hook.mjs (pattern proven by #2017) to a new surface. No new architecture.
  • Phase 2 generalizes the github-safe.js temp-file pattern (already shipped) to the agents that should have been using it. No new design.
  • Phase 3 is a mechanical pin upgrade gated by a static smoke. The upgraded version (@v4) is already what our own v3-ci.yml uses (lines 154, 158, 163 of the workflow).
  • Phase 4 mirrors the existing settings-generator.ts attribution logic into the helper generator. No new concept.

Init-template constraint: anything added to ruflo init output must work in an empty directory with only node and gh available. All four phases satisfy this: static markdown files and .js/.sh helpers — no new runtime dependencies installed into user projects.

Consequences

Positive

  • smoke-github-safe-injection.mjs catches the class of regression (#2017 shape) on the GitHub helper surface for the first time.
  • smoke-github-actions-pins.mjs catches any future commit that copies old @v3 snippets from blog posts into the skill files — a very common doc-drift pattern.
  • The swarm-pr.md / swarm-issue.md fix closes a prompt-injection path that could cause the GitHub Actions bot to execute attacker-controlled shell in a pull_request triggered workflow. The github-project-management skill already warned about this (skill lines 17-25); the gap was enforcement.
  • Attribution opt-in is now consistent: settings-generator.ts and the command file footers both respond to the same --attribution flag.
  • The supply-chain allowed-deps.json actions key extends an existing format that contributors already know how to update.

Negative / trade-offs

  • ADR-127 adds 3 new CI smoke jobs that gate every PR touching .claude/agents/github/, .claude/skills/github-*/, or v3/@claude-flow/cli/.claude/commands/github/. Purely-editorial PRs to those files (e.g. fixing a typo in a skill description) now run the smoke suite. The smokes are fast (static file reads, no npm install beyond root node_modules), but they add ~20–30 seconds to the pre-merge wall clock for any contributor touching those trees.
  • The ruvnet/swarm-action@v1 SHA-pin requires looking up the current HEAD SHA of that repo and re-doing this whenever the action is updated. Mitigation: add a note in .github/supply-chain/allowed-deps.json next to the entry.
  • Phase 2's tools: frontmatter additions are a behavioral change for any consumer who has configured ruflo init output and relies on WebFetch being available in GitHub agents. The downgrade is intentional and documented, but it is a breaking change on the init-template surface.
  • Phase 4's attribution gate is only enforced at init time. A user who edits their materialized command files after init can re-add the footer manually. This is acceptable — the commitment is to the default (init output without --attribution contains no footer), not to preventing all possible edits.

Neutral

  • The @v3@v4 pin upgrade is compatible with all runner images in v3-ci.yml (already running @v4 for the main CI jobs). No runner image changes needed.
  • The github-setup.sh set -euo pipefail addition is compatible with bash 3.x (macOS default), which the helper's current shebang (#!/bin/bash) already requires.

Implementation Plan

PhaseDeliverableFilesEffortAcceptance
1Injection smoke + Actions pin smoke + CI wiring + supply-chain updatescripts/smoke-github-safe-injection.mjs (new), scripts/smoke-github-actions-pins.mjs (new), .github/supply-chain/allowed-deps.json, .github/workflows/v3-ci.ymlSBoth smokes exit 0 in CI; v3-ci.yml path filters trigger on agent/skill changes
2Helper hardening + agent frontmatter + unquoted interpolation fixgithub-safe.js × 2, github-setup.sh × 2, 13 agent files × 2 (dogfood + init-template)Msmoke-github-safe-injection.mjs passes hardened copies; grep for unquoted interpolation returns 0 matches
3Deprecated-action smoke + @v3@v4 pin upgradescripts/smoke-github-release-no-deprecated-action.mjs (new), 6 agent files × 2, 2 skill filesSBoth new smokes pass; smoke-github-actions-pins.mjs passes all in-scope files
4Attribution gate in init-template + skill Last Updated stampsv3/@claude-flow/cli/src/init/helpers-generator.ts, 19 command files, 4 skill filesSruflo init without --attribution produces no 🤖 footer; ruflo init --attribution produces bot-identity footer

Recommended landing: Phases 1 + 3 in one PR (pure static checks + mechanical upgrades — no behavioral change). Phase 2 in its own PR (behavioral change to agents and helpers — needs explicit review). Phase 4 in its own PR (init-template behavior change).

Acceptance Criteria

The ADR is considered fulfilled when all of the following hold against @claude-flow/[email protected]:

  1. node scripts/smoke-github-safe-injection.mjs exits 0 with adversarial bodies against both handler copies.
  2. node scripts/smoke-github-actions-pins.mjs exits 0: all uses: refs in scope are SHA-pinned or on the allow-list.
  3. node scripts/smoke-github-release-no-deprecated-action.mjs exits 0: no deprecated action references survive.
  4. grep -r 'github.event.comment.body.*--comment\|github.event.label.name.*[^"]' .claude/agents/github/ v3/@claude-flow/cli/.claude/commands/github/ returns 0 matches.
  5. All 13 agent files have explicit tools: frontmatter; none include WebFetch in the set.
  6. ruflo init (no --attribution) produces 19 command files with no 🤖 Generated with string in any PR/issue body template.
  7. v3-ci.yml path filters include .claude/agents/github/**, .claude/skills/github-*/**, and v3/@claude-flow/cli/.claude/commands/github/**.
  8. No regression in the existing pre-bash-hook-smoke, plugin-registry-signature-smoke, or ruvllm-wasm-auto-init-smoke CI jobs.

Out of Scope (Deferred)

  • ADR-128: Provenance / witness signing for gh release create artifacts — generalizing the ADR-103 witness pattern to the release pipeline. This ADR establishes that the helper surface must not recommend deprecated Actions; it does not yet mandate Ed25519-signed release artifacts.
  • ADR-129: GitHub Apps token scoping — a separate decision about whether ruflo init-materialized workflows should request contents: read by default rather than inheriting the ambient GITHUB_TOKEN scopes. Gated on a broader permissions-minimization ADR.
  • ADR-130: SBOM generation in the release workflow. The supply-chain audit covers npm dep CVEs; an SBOM is a separate artifact type requiring a separate decision on format (SPDX vs CycloneDX) and storage location.
  • Dynamic action pinning automation (dependabot.yml or Renovate configuration for uses: refs) — net-new pattern with no prior art in this repo. Phase 3 does the manual upgrade; automation is a follow-on.