v3/docs/adr/ADR-127-github-stack-modernization.md
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
The .github-related surface in ruflo ships in two layers:
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.
Init-template layer — v3/@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:
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.
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.
tools: restrictions on 12 of 13 agentsThe 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.
--attribution opt-inv3/@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.
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.
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.mdv3/@claude-flow/cli/.claude/commands/github/*.mdFor 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:
"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:
# 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.
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:
# 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.
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.
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.
All four phases are connect-the-existing-pieces work:
smoke-pre-bash-hook.mjs (pattern proven by #2017) to a new surface. No new architecture.github-safe.js temp-file pattern (already shipped) to the agents that should have been using it. No new design.@v4) is already what our own v3-ci.yml uses (lines 154, 158, 163 of the workflow).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.
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.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.settings-generator.ts and the command file footers both respond to the same --attribution flag.allowed-deps.json actions key extends an existing format that contributors already know how to update..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.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.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.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.@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.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.| Phase | Deliverable | Files | Effort | Acceptance |
|---|---|---|---|---|
| 1 | Injection smoke + Actions pin smoke + CI wiring + supply-chain update | scripts/smoke-github-safe-injection.mjs (new), scripts/smoke-github-actions-pins.mjs (new), .github/supply-chain/allowed-deps.json, .github/workflows/v3-ci.yml | S | Both smokes exit 0 in CI; v3-ci.yml path filters trigger on agent/skill changes |
| 2 | Helper hardening + agent frontmatter + unquoted interpolation fix | github-safe.js × 2, github-setup.sh × 2, 13 agent files × 2 (dogfood + init-template) | M | smoke-github-safe-injection.mjs passes hardened copies; grep for unquoted interpolation returns 0 matches |
| 3 | Deprecated-action smoke + @v3 → @v4 pin upgrade | scripts/smoke-github-release-no-deprecated-action.mjs (new), 6 agent files × 2, 2 skill files | S | Both new smokes pass; smoke-github-actions-pins.mjs passes all in-scope files |
| 4 | Attribution gate in init-template + skill Last Updated stamps | v3/@claude-flow/cli/src/init/helpers-generator.ts, 19 command files, 4 skill files | S | ruflo 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).
The ADR is considered fulfilled when all of the following hold against @claude-flow/[email protected]:
node scripts/smoke-github-safe-injection.mjs exits 0 with adversarial bodies against both handler copies.node scripts/smoke-github-actions-pins.mjs exits 0: all uses: refs in scope are SHA-pinned or on the allow-list.node scripts/smoke-github-release-no-deprecated-action.mjs exits 0: no deprecated action references survive.grep -r 'github.event.comment.body.*--comment\|github.event.label.name.*[^"]' .claude/agents/github/ v3/@claude-flow/cli/.claude/commands/github/ returns 0 matches.tools: frontmatter; none include WebFetch in the set.ruflo init (no --attribution) produces 19 command files with no 🤖 Generated with string in any PR/issue body template.v3-ci.yml path filters include .claude/agents/github/**, .claude/skills/github-*/**, and v3/@claude-flow/cli/.claude/commands/github/**.pre-bash-hook-smoke, plugin-registry-signature-smoke, or ruvllm-wasm-auto-init-smoke CI jobs.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.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.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.