OOPUO
Review →
N.03
← Back to field notes
N.03 · Infrastructure · 16 AUG 2026

It reads strangers.
It holds secrets.

In August, three AI coding agents' default CI setups turned out reachable from a GitHub issue opened by an account with no repository access. The defect wasn't the model — it was one job built to read a stranger and hold the keys.

You wired a coding agent into your CI pipeline months ago — Claude Code, Gemini CLI, Codex, whichever ships fastest — because someone in the thread needed a faster loop and "tag the bot on the issue" beat waiting for a human reviewer. It has worked. Nobody looks at that workflow file anymore. It runs on an issue being opened, reads whatever a stranger typed, and holds the same runner credentials as your deploy job, because splitting them felt like process for its own sake. In August, three security teams showed that felt-like-process-for-its-own-sake was the whole defense.

How it forms

The reader and the holder are the same job.

A CI job earns both duties without anyone deciding it should. The trigger fires on an issue opening, a comment landing, a pull request title changing — content anyone can write, no repository access required. The same job's environment already carries whatever the runner needs to act: a deploy token, an API key, an OIDC credential it can trade for more. Splitting the job that reads from the job that acts takes an extra file, an extra needs:, a decision someone has to make on purpose.

Wiring a coding agent into that pipeline rarely starts as a decision about secrets at all. Someone wants the agent to answer on an issue, and the fastest path is the job that's already there — the one with deploy scope, because that job existed before the agent showed up. Three vendors reached that same shape independently in 2026. GMO Flatt Security's RyotaK reported in January that Claude Code's GitHub Action trusted any actor name ending in [bot] — trivial to spoof — to reach the credentials behind an OIDC token exchange; Anthropic's fix landed within days, per RyotaK's own writeup. A second, unrelated channel through the Read tool followed in April, fixed on 5 May per Microsoft's write-up. By August the same class of finding, now run against Gemini CLI and Codex as well, produced CVE-2026-54316. Three different products, three different channels, the same one job underneath doing both things.

Two questions only

What it reads, and what it's holding while it reads.

What untrusted input can reach this job?

List every trigger that lets outside content run before a person reads it: an issue opening, a comment landing, a pull request title, a discussion post. GitHub's own hardening guide warns that a pull_request_target workflow "may have repository write access and access to referenced secrets" by default, and recommends avoiding the trigger unless the job genuinely needs it. Every trigger on that list is a place attacker-controlled text enters before anything checks it.

Passes whenEvery trigger you listed points at a job with nothing else in its scope.

What does that job hold while it's reading?

Open the job itself — not the workflow, the job — and read its permissions block, its env block, and every step that injects a token. A default GITHUB_TOKEN already carries repository write scope unless the workflow sets read-only permissions explicitly. Two of the three August findings turned on exactly this staying invisible at runtime: Claude Code's command validator classified several read commands as safe in code no deployed configuration could override, and Gemini CLI's tool allowlist matched a name at registration but was never checked again once the tool ran — both per the researchers' own account.

Passes whenThe job that parses untrusted text holds no secret, no write-scoped token, and no step that can request one.

The split, in order

Three moves, in the order that matters.

  1. Keep one workflow, add a second job

    The job that triggers on an issue or PR and reads the body gets no secrets block and a read-only token. Everything that can write or deploy moves into a second, separate job — one that never receives the raw issue text as its input.

  2. Gate the handoff on a check the attacker can't talk to

    If the reading job has to pass a result forward, require a human approval or a deterministic check between the two jobs. "The agent said it's fine" is not a boundary; a required reviewer is.

  3. Default every token to read-only, then name the exceptions

    Set the workflow-level token to read-only by default, and grant anything wider only on the second job, only for the scope that job specifically needs — never across the whole workflow.

This section restates public vendor research about pipelines OOPUO did not build and has not tested. Checking a live workflow the same way starts with a written scope — and with your permission, OOPUO runs AI-assisted review only against the surfaces that scope names.

This one, specifically

Same job, still holding both?

That's the exact shape infrastructure hardening checks for first — a job that reads what a stranger wrote and still holds the keys to touch production. Send the workflow file that worries you.