Can One GitHub Issue Hijack Your npm Release? Lock Down CI

A GitHub issue nearly reached OpenAI's npm release. Learn to harden GitHub Actions against prompt injection, pwn requests, hijacked tags and stolen tokens.

TheTechLane X-ray scanner cover, large amber TL logo, an npm parcel hiding a GitHub issue marked prompt injection, headline Can one GitHub issue hijack your npm release

On 11 November 2026, the security firm Depi published a chain that started with an ordinary GitHub issue and ended at the publishing pipeline of the official @openai/codex npm package.

The entry point was not a memory bug. It was text, pasted into an AI agent's prompt by a GitHub Actions workflow that anyone could trigger.

This is the new shape of CI risk: your pipeline now runs agents that read issues, pull requests and comments, and every one of those is attacker-controlled input.

Supply-chain attacks on Actions were already real: in March 2025 the hijacked tj-actions/changed-files action hit over 23,000 repositories by repointing its version tags at a malicious commit.

In this guide, you'll learn how to:

  • Spot the workflow patterns that let a stranger steer your CI

  • Run AI coding agents in Actions without handing them your release keys

  • Lock down GITHUB_TOKEN, pin actions and keep caches out of release jobs

  • Publish to npm with OIDC trusted publishing instead of long-lived tokens

  • Respond fast if a workflow or package is compromised


Quick Answer

Threat in one line: Any workflow that feeds issue, PR or comment text into a shell command or an AI agent, while secrets or write tokens sit nearby, can be turned against you.

Fix in one line: Treat all event text as untrusted, give agents no tools and no secrets when they read it, scope every token to the minimum, pin dependencies and publish only through a protected OIDC release job.

Here are the defence layers, outside in:

Untrusted text (issue, PR, comment)
  -> trigger and author filters (who can start the job)
  -> no inline ${{ }} in run:, env vars only
  -> agent sandbox: tools off, read-only, structured output
  -> GITHUB_TOKEN: permissions {} by default
  -> pinned actions, no shared cache in release jobs
  -> release job: environment approval + npm trusted publishing (OIDC)

How the Attack (or Leak) Happens

Each vector below is documented by GitHub or seen in a real incident.

Prompt injection through an AI agent

Depi found that the openai/codex Issue Labeler workflow passed ${{ github.event.issue.title }} and ${{ github.event.issue.body }} straight into the Codex agent's prompt, with allow-users: "*", according to Depi's write-up.

A crafted issue could make the agent read the body as new instructions and run commands inside its sandbox.

The OWASP Top 10 for LLM Applications lists this class, prompt injection, as risk number one.

Sandbox escape and token theft

Depi's Bugcrowd disclosure summary says the team escaped a workspace-write sandbox through .NET debug pipes in /tmp/ and read GITHUB_TOKEN and ACTIONS_RUNTIME_TOKEN from runner memory.

The lesson is simple: an agent that reads untrusted text should never share a runner with credentials it could reach.

Cache poisoning into a privileged workflow

With the runtime token, an attacker can write a poisoned cache entry that a more privileged workflow later restores and executes.

Depi used this to pivot into rust-ci.yml, a workflow with write permissions, a technique first detailed in Adnan Khan's cache poisoning research.

Hijacked release identity through OIDC

The final step was a planted workflow with id-token: write that fires when a release is deleted, so a maintainer cleaning up a fake release would hand over a signed OIDC token that npm trusts for publishing.

OpenAI fixed the issue, and its public fix commit replaced the agent with a Responses API call that has tools disabled and a strict JSON schema; Depi reports an initial $1,000 bounty and no malicious release.

Script injection in run: steps

You don't need an AI agent to get owned.

GitHub's script injection guide shows how ${{ github.event.pull_request.title }} inside run: is pasted into the shell before it runs, so a title can become a command.

Pwn requests and moved tags

The pull_request_target and workflow_run triggers run with base-repo secrets; checking out untrusted PR code in them is the classic pwn request.

Mutable tags like @v4 are the other gap, which is exactly how tj-actions/changed-files leaked secrets into workflow logs.

UML sequence diagram of the OpenAI Codex GitHub Actions chain disclosed by Depi: an attacker opens a GitHub issue, the Issue Labeler workflow pastes it into an AI agent prompt, the agent escapes its sandbox and leaks runner tokens, a poisoned Actions cache runs code in a privileged workflow, and a release trap yields an OIDC token npm trusts, with the defences that break each step

One GitHub issue to an npm publish token: the attack path and where to break it


Who Is at Risk?

You are in scope if any of these describe your setup:

  • Open source maintainers running AI triage, labelling or review bots on issues and PRs

  • Teams using pull_request_target or workflow_run to comment on or label fork PRs

  • npm, PyPI or container publishers who release from GitHub Actions

  • Repos referencing third-party actions by tag or branch instead of commit SHA

  • Organisations whose default GITHUB_TOKEN still has read and write access

  • Developers who auto-update CLI tools and agents the moment a release lands

A private repo is not safety: insiders, compromised contributor accounts and poisoned third-party actions all run inside it.


How to Detect It

Start with an audit of what you already run.

zizmor static analysis

zizmor is an open source static analyser for GitHub Actions that flags template injection, dangerous triggers, excessive permissions, unpinned actions and cache poisoning risks.

Run it against your workflow folder:

uvx zizmor .github/workflows/

Inline expressions in run: blocks

A quick search shows where event text is expanded directly.

Search your workflows:

grep -rn 'github.event' .github/workflows/

Any hit inside a run: block, or inside an agent prompt:, deserves a review.

Default token permissions

Check whether new workflows get read-only or read-write tokens by default, using the GITHUB_TOKEN permissions docs as reference.

Query the repository setting:

gh api repos/OWNER/REPO/actions/permissions/workflow

npm registry signatures and provenance

On the consumer side, npm can verify registry signatures and provenance attestations of installed packages, as described in npm's provenance docs.

Run it in a project after install:

npm audit signatures

How to Prevent It

Apply these in order; each one closes a link in the chain above.

1. Never expand event text inline

Follow GitHub's secure use reference and pass untrusted values through an intermediate environment variable.

Use this pattern:

- name: Check PR title
  env:
    TITLE: ${{ github.event.pull_request.title }}
  run: |
    if [[ "$TITLE" =~ ^octocat ]]; then echo "ok"; fi

2. Give agents text, not tools

If an AI agent reads untrusted input, run it with no shell, no network, no secrets beyond its own API key, and force structured output you validate, just as OpenAI's fix does.

For openai/codex-action, keep allow-users empty (write-access users only) and the default safety-strategy: drop-sudo or stricter; never use unsafe on public triggers.

3. Start every workflow at zero permissions

Set permissions: {} at the top of each workflow, then grant each job only what it needs.

Also switch the repository and organisation default to read-only.

4. Keep privileged triggers away from untrusted code

Read Securely using pull_request_target before using it, and never check out or build the PR head in that context.

Prefer plain pull_request, which gets a read-only token and no secrets for forks.

5. Pin every action to a full commit SHA

A full SHA can't be repointed like a tag; GitHub's docs now also describe workflow dependency lockfiles that pin actions, including transitive ones.

Let Dependabot raise pinned action updates so pins don't rot.

6. Keep caches out of release jobs

GitHub's dependency caching reference explains how caches are shared across workflows on a branch.

Build release artifacts from a clean checkout and never restore a cache in the job that publishes.

7. Publish with OIDC trusted publishing

npm trusted publishing swaps long-lived tokens for short-lived OIDC credentials and generates provenance automatically.

Bind it to one workflow file and a GitHub environment with required reviewers, so a stray id-token: write elsewhere can't publish.

8. Add a cooldown before auto-updates

Depi's own advice is a short delay before auto-updating agents and tooling, giving maintainers time to catch a bad release before it lands on your laptop.


Hands-On Setup: Harden an npm Release Workflow

This walkthrough audits a repo, locks the token default and moves publishing to trusted publishing.

1. Install zizmor

Install it as a uv tool (Homebrew, pipx and cargo also work, see the installation docs):

uv tool install zizmor

Expected result:

zizmor is installed and on your PATH; run zizmor --version and check for the latest release

2. Audit your workflows

Run it from the repository root:

zizmor .github/workflows/

Expected result:

Findings grouped by audit, for example error[template-injection] or unpinned-uses, each pointing at a file and line

3. Make the default token read-only

Set the repository default with the GitHub CLI (organisation admins can do this once for every repo in settings):

gh api --method PUT repos/OWNER/REPO/actions/permissions/workflow -f default_workflow_permissions=read

Expected result:

No output on success; re-run the GET from the detection step and default_workflow_permissions shows read

4. Rewrite the release workflow

This follows npm's trusted publishing example, with zero default permissions, one protected job and SHA pins (replace the placeholders with real 40-character SHAs).

Save it as .github/workflows/release.yml:

name: Release
on:
  push:
    tags: ['v*']
permissions: {}
jobs:
  publish:
    runs-on: ubuntu-latest
    environment: npm-release
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@<full-commit-sha> # v6
        with:
          persist-credentials: false
      - uses: actions/setup-node@<full-commit-sha> # v6
        with:
          node-version: '24'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm test
      - run: npm publish

5. Register the trusted publisher

On npmjs.com, open your package Settings, add GitHub Actions as a trusted publisher with your owner, repository, release.yml and the npm-release environment.

Then select "Require two-factor authentication and disallow tokens" under Publishing access, as npm recommends, and revoke old automation tokens.

Expected result:

The next tag push publishes without NPM_TOKEN, and the npm package page shows a provenance badge

6. Keep pins fresh with Dependabot

Add the github-actions ecosystem to .github/dependabot.yml:

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Expected result:

Dependabot opens PRs that bump each pinned SHA, with the version in the trailing comment

Incident Response Playbook

Assume a workflow ran attacker-controlled code; move in this order.

1. Contain the workflow

Disable the vulnerable workflow immediately:

gh workflow disable "Issue Labeler"

2. Revoke publishing credentials

List and revoke npm tokens, and rotate every secret the workflow or its repo could read:

npm token list

Revoke each suspicious entry with npm token revoke <id>, and remove extra trusted publisher configs you don't recognise.

3. Purge caches and rogue workflows

Delete the repository's Actions caches so no poisoned entry is restored:

gh cache delete --all

Review recent commits under .github/workflows/, unexpected releases and new deploy keys before deleting anything that could trigger a booby-trapped workflow; disable Actions first if unsure.

4. Audit what was published

Check every version released in the exposure window, and deprecate any bad one:

npm deprecate my-pkg@1.2.3 "Compromised build, upgrade to 1.2.4"

5. Prevent the repeat

Ship the fixes from How to Prevent It, re-run zizmor in CI on every PR, and write a short public advisory if users may have installed a bad version.

Flowchart of a CI compromise incident response playbook: disable the workflow, revoke npm tokens and rotate secrets, delete Actions caches, remove rogue workflow files or releases only after workflows are disabled, deprecate any bad npm version, then harden with zero default permissions, SHA pins, cache-free release jobs, OIDC publishing with approval and zizmor on every PR

Your CI compromise playbook, in order


Common Mistakes

Mistake 1: Trusting the agent's guardrails

Depi bypassed guardrails with an encoded payload; design as if the agent will obey the attacker.

Mistake 2: allow-users set to everyone

A wildcard turns every GitHub account into a prompt author for your CI.

Mistake 3: Pinning by tag and calling it pinned

Tags can move, as tj-actions proved; only a full SHA or lockfile is immutable.

Mistake 4: id-token: write at workflow level

Granting OIDC to every job means any compromised job can mint a publishing identity.

Mistake 5: Deleting suspicious releases first

In the Codex chain, deleting the fake release was the trigger; contain workflows before cleaning up.


Security Checklist

  • permissions: {} at the top of every workflow

  • Org and repo default GITHUB_TOKEN set to read

  • No ${{ github.event.* }} inside run: or agent prompts without an env var

  • AI agents on public triggers run with tools off, no secrets and validated output

  • No pull_request_target that checks out PR code

  • Every third-party action pinned to a full SHA or lockfile, updated by Dependabot

  • No cache restore in release jobs

  • npm trusted publishing with an approval environment, tokens disallowed

  • zizmor running on every PR

  • Short cooldown before auto-updating CLI agents

Get Security Alerts in Your Inbox

Supply-chain incidents move fast, and the fix is usually one config line away.

Join the TheTechLane newsletter for practical security guides and patch alerts every week.


FAQ

Do I need to patch anything after the Codex disclosure?

No, Depi says OpenAI fixed the core bug and users have nothing to patch.

The real takeaway is to audit your own workflows for the same pattern.

Is pull_request_target always dangerous?

No, it is safe for labelling or commenting as long as it never checks out, builds or runs the PR's code.

Read GitHub's dedicated guide before using it.

Does OIDC trusted publishing stop all package hijacks?

It removes long-lived tokens, but anyone who can run code in the authorised workflow can still publish.

That is why the release job needs an approval environment, SHA pins and no caches.

Can I safely let an AI agent triage public issues?

Yes, if it only returns structured data such as labels, has no tools or secrets, and your workflow validates the output before applying it.

How is this different from leaking API keys?

Secret leaks expose credentials you already have; this attack makes your own pipeline mint or use them, covered in our separate secrets guide below.


Key Takeaways

Here is what to remember:

  • One GitHub issue was enough to start a chain toward OpenAI's npm release

  • Event text is untrusted, whether it reaches a shell or an AI agent

  • Least-privilege tokens, SHA pins and cache-free release jobs break the chain

  • npm trusted publishing plus an approval environment beats long-lived tokens

Your CI is production: give untrusted text zero power and your release job zero neighbours.


Related Reading on TheTechLane


Sources


Follow TheTechLane

Keep learning AI, development, and modern developer technologies with TheTechLane.

Learn • Build • Share

Follow @thetechlanehq for more developer guides, tutorials, and practical technology content.