harnessguard

September 20, 2026 · View on GitHub

Static hardening linter for GitHub Actions workflows that run AI agents.

Agents in CI (Claude Code Action, Gemini CLI Action, Codex, Copilot agents and friends) read attacker-controlled text — issue bodies, PR titles, comments — while the runner holds secrets and write tokens. That combination is the new CI/CD supply-chain attack surface: a public comment becomes instructions the agent obeys.

harnessguard enforces Microsoft's Agents Rule of Two: an AI-powered workflow must never combine untrusted input, privileged access, and external communication. It runs as a static, deterministic check in pull requests — it never executes your workflows or your agents.

Why

Real incidents, all from 2026:

  • Comment and Control (April 2026) — hijacked Claude Code Security Review, Gemini CLI Action and Copilot Agent via PR titles / issue bodies; leaked ANTHROPIC_API_KEY, GEMINI_API_KEY and GITHUB_TOKEN through public comments.
  • Black Hat USA 2026 (August) — an unprivileged GitHub issue reached CI runner secrets in the vendors' own repositories; Gemini CLI Action got CVE-2026-12537 (CVSS 10.0).
  • MSRC — Claude Code Action's Read tool reached /proc/self/environ; fixed in 2.1.128.

Existing Actions linters (zizmor, actionlint, poutine…) are excellent but agent-blind: they don't model "this step is an LLM that will obey text."

Research

We sampled 337 public agent workflow files (255 repos) via the GitHub code search API: 44.2% combine untrusted events with runner secrets or write permissions, and 61% of those carry no actor-association guard in the workflow file. Aggregates only — no repo is named.

Pass 2 re-scanned the full workflow set of the same 255 repos for cross-workflow artifact chains (HG007): 5.5% carry the chain, and every chain detected runs no agent on either side — a blind spot for agent-only scanners and per-file linters alike.

Pass 2.5 looked for the mirror pattern (HG008): untrusted-triggered workflows that pass secrets into an agent-bearing reusable workflow. 20 repos carry the chain; 17 of 20 come from the Gemini CLI Action dispatch template. MCP hygiene (HG010): 5 unpinned MCP server configurations, none pinned.

Pass 3 re-fetched the pass-1 corpus with the same engine three days later: 97% of files are byte-identical — zero findings fixed, zero introduced.

Pass 4 re-ran the pipeline with new queries (MCP configuration, agentic workflows, newer agent CLIs): 108 agent files from 87 repos, ~90% new material. 21% launch unpinned MCP servers (query-biased sample; see the report caveats). Validating the sample caught three HG010 false-positive classes and a missing agent action, fixed in 0.6.2–0.6.4. → State of AI-agent workflows in the wild · reproduce with uv run python scripts/ecosystem_scan.py, scripts/chain_scan.py and scripts/remediation_scan.py

Install

pip install harnessguard
# or from source
uv sync && uv run harnessguard --version

Usage

# scan the current repo (finds .github/workflows/)
harnessguard scan .

# fail CI on high and above, write SARIF for code scanning
harnessguard scan . --fail-on high --sarif harnessguard.sarif

# machine-readable output
harnessguard scan . --format json

# adopt on an existing repo: record current findings, then only fail on new ones
harnessguard scan . --format json > baseline.json
harnessguard scan . --baseline baseline.json --fail-on high

# let coding agents lint the workflows they generate (stdio MCP server)
harnessguard mcp

# list rules
harnessguard rules list

Exit code is 1 when findings at or above --fail-on exist. --fail-on none reports without failing.

MCP server

harnessguard mcp serves the scanner over stdio MCP (read-only, no network): scan_repository, scan_workflow (lint YAML before writing it) and list_rules. Wire it into any MCP client:

{ "mcpServers": { "harnessguard": { "command": "uvx", "args": ["harnessguard", "mcp"] } } }

GitHub Action

name: harnessguard
on: [push, pull_request]

jobs:
  harnessguard:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: dtduc-git/harnessguard@v0.6.4
        with:
          fail-on: high

Rules

RuleSeverityWhat it catches
HG001criticalAgent step on untrusted events with secrets in scope (job or workflow env)
HG002highAttacker-controlled event data interpolated into an agent step
HG003highAgent step on untrusted events with write permissions
HG004highAgent step in a pull_request_target workflow
HG005mediumAgent step with shell/network tool grants or egress commands
HG006highAgent job checks out an attacker-controlled ref
HG007highPrivileged workflow_run job consumes artifacts from untrusted-triggered workflows (Cordyceps chain)
HG008highUntrusted-triggered workflow passes secrets into an agent-bearing reusable workflow
HG009highAgent step consumes artifacts from an untrusted-triggered workflow
HG010mediumAgent step launches unpinned MCP servers or plaintext MCP endpoints

Findings map to the OWASP Top 10 for Agentic Applications (ASI01–ASI05). Jobs whose if: restricts triggering via github.actor / author_association guards get HG001/HG003 downgraded one level — reduced exposure is still flagged, just at lower severity. Rules are data — YAML in src/harnessguard/rules_data/ — and checks are small named functions in checks.py. Add your own with --rules-dir.

Design principles

  • Never executes anything. No workflow runs, no agent calls, no network.
  • Local-first. No account, no telemetry, no SaaS.
  • Deterministic. Same input → same findings; no LLM in the detection path.
  • Rules as data. Extend coverage without touching the engine.

Non-goals

  • Not a general-purpose Actions linter — use zizmor alongside it.
  • No runtime enforcement or proxy — that is a different (complementary) tool.
  • Not GitLab/Jenkins (yet).

References

  • Microsoft Security Blog — Securing CI/CD in an agentic world (Agents Rule of Two), June 2026
  • CSA — Comment and Control: GitHub AI Agents as Credential Exfiltrators, April 2026
  • OWASP — Top 10 for Agentic Applications 2026

License

Apache-2.0