# Running AI coding agents in GitHub Actions without leaking secrets

> Why AI agents in CI leak secrets through PR titles and issue comments, and the workflow design that contains it: triggers, token scopes, and split jobs.

- Published: 2026-10-18
- Author: Enforster AI (https://enforster.ai)
- Category: AI Security
- Tags: AI Security, Prompt Injection, Supply Chain, Access Control, Secure Coding
- Canonical URL: https://blog.enforster.ai/ai-coding-agents-in-github-actions-without-leaking-secrets

## Key takeaways

- An AI agent in CI that reads pull request titles, issue bodies, or comments is processing attacker-controlled input, so any secret in the same job should be assumed reachable.
- In 2026 researchers showed prompt injection through PR titles and issue comments exposing credentials from the Claude Code Security Review Action, the Gemini CLI Action, and GitHub Copilot Agent.
- Run agents that read untrusted content under the pull_request trigger with a read-only token, not under pull_request_target, which exposes secrets and a write token to fork pull requests.
- Split the work: an unprivileged job lets the agent produce output as an artifact, and a separate privileged job validates and publishes it without running the model.
- Never let anyone without write access trigger an agent, and never interpolate event text directly into a shell script or prompt template.

---

If an AI coding agent in your GitHub Actions workflow reads pull request titles, issue bodies, or comments, it is reading text an attacker wrote, and anything in that job's environment is within reach of a successful prompt injection. You cannot make the model reliably ignore those instructions. What you can do is design the workflow so the job the agent runs in holds nothing worth stealing and cannot write anywhere that matters.

This is no longer a theoretical concern. Two research disclosures, a year of CI supply chain incidents, and an industry research note all point to the same pattern. This article explains the pattern and gives a workflow design that contains it.

## What actually went wrong in the published attacks?

In December 2025, Aikido Security published [PromptPwnd](https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents), describing a class of vulnerabilities in CI pipelines that use AI agents. The pattern has two halves: untrusted text such as issue titles, PR bodies, or commit messages is inserted into the agent's prompt, and the agent runs with privileged tokens and tools that can run shell commands or edit issues. Aikido named Gemini CLI, Claude Code Actions, OpenAI Codex Actions, and GitHub AI Inference integrations, and reported that Google's own Gemini CLI repository was affected and patched within four days of disclosure.

In April 2026, security researcher Aonan Guan, with Johns Hopkins researchers Zhengyu Liu and Gavin Zhong, documented an attack they called "Comment and Control", [reported by SecurityWeek](https://www.securityweek.com/claude-code-gemini-cli-github-copilot-agents-vulnerable-to-prompt-injection-via-comments/). Against three agents:

- **Claude Code Security Review Action:** a crafted PR title led the agent to run commands and surface credentials in a security finding or the Actions log.
- **Gemini CLI Action:** issue comments carrying an injection bypassed guardrails and exposed a full API key.
- **GitHub Copilot Agent:** a payload hidden in an HTML comment bypassed environment filtering and the network firewall, after a maintainer assigned the issue to Copilot.

SecurityWeek reports that GitHub classified its case as a known architectural limitation, and that Anthropic's and Google's fixes changed the agent's allowed tools and its system prompt respectively. That is the honest answer: model-side mitigations reduce how often an injection works, but they do not change what the job can reach when one does.

> **Guardrails are not the boundary:**
>
> A system-prompt guardrail or a filtered tool list is a mitigation inside the model's decision loop. Treat it as defense in depth, and put the real boundary in the workflow: which trigger runs the job, which token it gets, and which secrets exist in its environment.

## Why is an agent in CI riskier than a normal workflow step?

Classic Actions injection is about a shell expression: interpolate `github.event.pull_request.title` into a `run:` script and an attacker can break out of the string. GitHub's [secure use reference](https://docs.github.com/en/actions/reference/security/secure-use) fixes that with an intermediate environment variable, because the value then "doesn't interact with the script generation process."

An agent removes that fix. Its whole job is to read the text and act on it, so there is no quoting that makes instructions inert. The Cloud Security Alliance's [research note on AI-powered GitHub Actions](https://labs.cloudsecurityalliance.org/research/csa-research-note-ai-github-actions-security-20260503-csa-st/) makes the same point: the workflow's behavior is now decided at runtime by a language model rather than by fixed conditional logic. The same note lists the surrounding failure modes that turn an injection into an incident: over-privileged default tokens, floating action tags, and secrets encoded to evade log masking.

This is the CI version of a principle we argued in [Prompt injection is an access-control problem](https://blog.enforster.ai/prompt-injection-is-an-access-control-problem): the model may propose actions, but something it cannot influence must decide what it is allowed to touch.

## Which trigger should an agent workflow use?

The single most important choice is the event trigger. GitHub Security Lab's guide to [preventing pwn requests](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/) spells out the difference:

| Trigger | Fork PRs get secrets? | Token | Use for agents? |
| --- | --- | --- | --- |
| `pull_request` | No | Read-only for forks | Yes, for anything that reads PR content |
| `pull_request_target` | Yes | Read/write | No, unless the agent never sees PR content |

`pull_request_target` runs in the context of the base repository, with secrets and a write token, even for pull requests from forks. Security Lab calls combining it with a checkout of the PR "a dangerous practice that may lead to repository compromise." An agent that needs an API key and permission to post a review is tempting to wire to this trigger, and that is exactly the combination of untrusted text, secrets, and write access the attacks exploited. Comment and issue triggers deserve the same caution: Aikido notes that some vulnerable workflows could be started by any external user who files an issue.

## What does a contained agent workflow look like?

Split the work into two workflows, following the pattern Security Lab recommends for builds that need write access:

1. **An unprivileged job** triggered by `pull_request`. It checks out the code, runs the agent, and uploads the agent's output as an artifact. It has a read-only token and only the secrets the agent strictly needs, ideally a short-lived, low-limit API key.
2. **A privileged job** triggered by `workflow_run`. It downloads the artifact, validates it as data, and posts the comment. It never runs the model and never executes anything from the artifact.

```yaml
# .github/workflows/agent-review.yml  (unprivileged)
name: agent-review
on: pull_request
permissions:
  contents: read
jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<full-commit-sha>
        with:
          persist-credentials: false
      - name: Run review agent
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          MODEL_API_KEY: ${{ secrets.REVIEW_AGENT_KEY }}
        run: ./scripts/run-review-agent.sh > review.json
      - uses: actions/upload-artifact@<full-commit-sha>
        with:
          name: review
          path: review.json
```

```yaml
# .github/workflows/agent-review-publish.yml  (privileged, no model)
name: agent-review-publish
on:
  workflow_run:
    workflows: [agent-review]
    types: [completed]
permissions:
  pull-requests: write
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@<full-commit-sha>
        with:
          name: review
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ github.token }}
      - name: Validate and post
        run: python3 scripts/post_review.py review.json
```

Note that on fork pull requests the `pull_request` job receives no repository secrets at all, so reviewing fork contributions needs an agent that works without a key, or a maintainer who moves the change onto a branch inside the repository first. That limitation is the point: the fork author should not be able to cause your key to be loaded next to their text.

> **Validate the artifact like an API request:**
>
> The publishing script should parse the agent's output against a strict schema, cap its length, strip anything that looks like a credential, and post only to the pull request that produced it. Security Lab is explicit that artifacts from untrusted runs are untrusted and must never be executed in the privileged context.

## What else belongs on the checklist?

Beyond the trigger and the split, these controls come straight from the sources above:

- **Declare `permissions` in every workflow.** GitHub recommends setting the default `GITHUB_TOKEN` to read-only for contents and raising permissions per job only where needed.
- **Gate who can trigger the agent.** Aikido flags settings that remove the write-access check, such as `allowed_non_write_users: "*"` for Claude Code and `allow-users: "*"` for Codex. Restrict agent triggers to users with write access.
- **Never interpolate event text into scripts or prompt templates.** Pass it through an environment variable or a file, and label it in the prompt as untrusted content. This does not stop injection, but it closes the shell-level bug.
- **Restrict the agent's tools.** Remove shell access, process listing, network egress, and the ability to write to issues or PRs from any job that reads untrusted content.
- **Pin actions to a full commit SHA.** GitHub notes that a tag "can be moved or deleted"; the CSA note lists tag hijacking among the incidents that defined the past year.
- **Set `persist-credentials: false` on checkout,** so the token is not left on disk for later steps or a compromised agent to read.
- **Scope and shorten API keys.** Use a dedicated key per workflow with a low spending limit, and rotate it if a run ever behaves unexpectedly.
- **Test it.** Open a PR whose title asks the agent to print its environment, and confirm nothing sensitive comes back. Keep that PR as a regression test and re-run it whenever the agent, its prompt, or its tool list changes.

## How does this relate to developer machines?

The same reasoning applies outside CI. The August 2025 Nx "s1ngularity" compromise, summarized in the CSA note, used malware that targeted AI coding tool credentials on developer machines. Coding agents on a laptop and in a runner share a failure mode: they act with the user's or the pipeline's authority while reading content someone else wrote. In both places the control is the same. Decide what the agent can reach before it reads anything, because after it reads attacker text you no longer decide what it does.

> **Scope of this guide:**
>
> This article covers GitHub Actions. The same split between an unprivileged agent job and a privileged publishing job applies to GitLab CI and other runners, but trigger names and secret-exposure rules differ, so check your platform's documentation.
