Code Security

Claude Code Action Bug: Security Flaw Lets Triage Roles Inject Data

Amartya | CodeAnt AI Code Review Platform
Sonali Sood

Founding GTM, CodeAnt AI

  • Finding: HackerOne #3918594

  • Tested product: Claude Code Action v1.0.185 at commit 9db594c7a0e82298c121c18b7f08aa1579ce7341

  • Vendor severity: High, CVSS 4.0 score 7.5

Executive summary

Claude Code Action can start from a GitHub issue label. Before it gives Claude access to repository-writing tools, the Action checks whether the actor in the webhook has write permission. It also filters comments to the trigger time so that content added after authorization cannot silently enter the running job.

In v1.0.185, those two controls were bound to different events.

The permission check used the actor from the original webhook. For issues.labeled, however, the Action did not take the event time directly from that webhook. It fetched the issue's live event history, found every labeled event with the same label name, and selected the newest one. The lookup did not require the selected event to have been created by the actor who passed the permission check.

This created an identity-rebinding vulnerability. After a maintainer applied the trigger label, a collaborator with GitHub's triage role could add data, remove the label, and apply the same label again. The maintainer's already-authorized run could then mistake the triage user's newer event for its own. Its comment cutoff moved forward, and data that was correctly excluded at the original authorization boundary entered the final prompt.

The lower-privileged collaborator still could not invoke the repository-writing workflow directly. Their own webhook failed the Action's write-permission gate. The capability gained through the bug was narrower and more important: control of post-authorization input consumed by a maintainer-authorized run that held edit, commit, and push capabilities.

Why the trigger time matters

Agentic GitHub workflows frequently combine two different classes of data:

  1. an authorization event, such as a maintainer applying a trusted label; and

  2. live repository context, such as issue comments, reviews, and pull request discussion.

Fetching live context is convenient, but it creates a race. A user can change that context while a queued or starting job is still preparing its prompt. Claude Code Action therefore applies a temporal cutoff. Comments created or last edited after the trigger time should not be included in the run.

The intended invariant is:




That invariant only holds if T belongs to the webhook that was actually authorized.

For label events, the implementation had to reconstruct T from GitHub's issue-event API. The relevant resolver was equivalent to:

const matches = events.filter(
  event => event.event === "labeled" && event.label?.name === labelName
);
const latest = matches.sort(byCreatedAtDescending)[0];
return latest?.created_at;
const matches = events.filter(
  event => event.event === "labeled" && event.label?.name === labelName
);
const latest = matches.sort(byCreatedAtDescending)[0];
return latest?.created_at;
const matches = events.filter(
  event => event.event === "labeled" && event.label?.name === labelName
);
const latest = matches.sort(byCreatedAtDescending)[0];
return latest?.created_at;

GitHub's event representation includes an event actor and an event identifier. Neither was used to bind the selected event to the original webhook. Event type, label name, and recency were treated as an identity.

This is the same class of trigger and authorization surface we cover in our CI/CD security research: a pipeline decision that looks like a single, atomic gate is actually two decisions stitched together across a live data source, and the seam between them is where these bugs live.

Two security decisions, two different objects

The defect is easier to see when the job is split into its two decisions.

Decision 1: may this actor start a write-capable run?

The Action checked context.actor from the webhook. A maintainer with write permission passed. A triage collaborator did not.

Decision 2: which repository context belongs to that authorized run?

The Action queried the current issue-event history. It selected the most recent event with the same action and label name, regardless of actor. That timestamp became the cutoff used to collect comments for the final prompt.

The first decision authorized one lifecycle object. The second decision consumed another.




Once those identities diverged, a user who could not pass the first decision could influence the result of the second.

Reproducing the boundary shift

The deterministic test used the following timeline:




At 10:01, the comment does not exist. It must therefore remain outside the maintainer run.

With only the original event available, the resolver returns 10:01 and the comment is excluded. When the later same-label event is present, the resolver returns 10:03. The exact same comment now appears to be pre-trigger data and enters the prompt.

The attacker does not need to edit the original webhook or impersonate the maintainer. They only need legitimate issue-management operations available to their lower role. The original job performs the substitution itself when it reads mutable event history.

The protected boundary and capability delta

The security claim is not that issue comments are trusted. They are expected to be untrusted input. It is also not that triage collaborators can manage labels. GitHub intentionally grants them that capability.

The protected boundary is the snapshot created by a maintainer's authorization event. Claude Code Action explicitly filters later content so that the inputs to a privileged run cannot change after that event.

The complete capability transition was:

Element

Proven value

Attacker principal

Collaborator with GitHub's triage role

Existing capabilities

Comment on issues and manage labels

Direct authorization

Rejected by Claude Code Action's write-permission gate

Protected boundary

Maintainer webhook and its trigger-time context snapshot

Identity error

A newer same-name event from another actor replaces the authorized event's temporal identity

Privileged sink

Tag mode with workspace edits, scoped commit tools, and authenticated push

New capability

Mutate the model input of an already-authorized repository-writing run after authorization

This distinction is what separates the issue from ordinary prompt injection. The value was not merely present in an issue that Claude chose to read. It was created after the explicit cutoff and reached the run only because the cutoff was rebound to a different user's event.

From input mutation to a repository write

The proof was divided into two layers so that each claim could be tested independently.

Layer 1: exact resolver and prompt construction

The first harness imported the release's unmodified implementations of:

  • resolveTriggerTimestamp();

  • fetchGitHubData();

  • the write-permission check;

  • context preparation; and

  • final prompt generation.

Deterministic Octokit responses represented the maintainer and triage roles. The harness confirmed that:

  • the maintainer webhook passed the exact permission gate;

  • the triage collaborator's direct webhook failed it;

  • a later same-label event from the triage actor moved the cutoff;

  • the post-trigger value entered the exact final prompt only after that move;

  • the original-event-only control excluded the value; and

  • a later event for a different label also excluded it.

Three clean runs produced identical prompt hashes for the positive and negative cases.

Layer 2: the repository-writing sink

The second harness used the Action's bundled Claude Code runtime and its actual tag-mode tool configuration. All model traffic was loopback-only, every credential was synthetic, and the Git remote was a local bare repository.

The issue body contained a maintainer-authored task: copy the latest RELEASE_APPROVAL value from the supplied context into release-approval.txt, commit it, and push it. The attacker comment contained only a data value. It did not ask the model to ignore instructions or perform a malicious action.

In every positive run, event rebinding admitted the value, Claude wrote it, committed it, and invoked the release's push helper. In every matched negative run, the trusted task remained present but the post-trigger value was absent, so no file and no remote branch were created.

Differential evidence

The controls were designed to change one security-relevant property at a time.

Case

Permission result

Selected cutoff

Attacker value

Repository sink

Triage actor triggers directly

Rejected

Not reached

Not reached

No write

Maintainer event only

Allowed

10:01

Excluded

No attacker-selected file

Later different-label event

Allowed

10:01

Excluded

No attacker-selected file

Later same-label event from triage actor

Allowed under maintainer webhook

10:03

Included

File committed and pushed

Same-label event with actor binding enforced

Allowed under maintainer webhook

10:01

Excluded

No attacker-selected file

The exact positive and negative matrices passed three consecutive clean runs each. The sink proof recorded zero permission denials because the Action believed it was still carrying out the maintainer-authorized job.

Impact

A repository that used issues.labeled with a Claude trigger label and supplied a write-capable workflow token could expose its authorized job to post-authorization input mutation by a lower-privileged collaborator.

Depending on the maintainer-authored task and repository automation, the admitted data could influence:

  • generated source or configuration;

  • release metadata and approval files;

  • commits produced by Claude;

  • branches pushed by the Action; and

  • downstream CI or deployment behavior that consumes those commits.

The finding was rated High, not Critical. Exploitation required a suitable label-triggered workflow, a collaborator with triage permissions, a timing window before the authorized job completed its live lookup, and a task in which the admitted data influenced output. The report did not claim that every injected value automatically becomes arbitrary code execution.

Anthropic assigned CVSS 4.0 score 7.5:

This finding, along with the reconstructed resolver logic and proof-of-concept detail, is catalogued in the CodeAnt AI vulnerability database for teams tracking the CVE alongside their own GitHub Action workflows.

Root cause

The implementation used mutable semantic attributes as a substitute for event identity.




The fields event === "labeled" and label.name === "claude" describe a class of events. They do not identify one event. Selecting the newest member of that class makes the boundary sensitive to legitimate actions performed after authorization.

This is a lifecycle authorization problem. The permission decision was correct when it ran, and the comment filter was correct for the timestamp it received. The vulnerability appeared in the transition between them, where the timestamp's provenance was lost.

Recommended remediation

The durable fix is to carry one authenticated event identity through authorization, context collection, and execution.

  1. Prefer an immutable event identifier supplied by a trusted delivery field when GitHub exposes one for the trigger.

  2. If a live lookup is unavoidable, require the selected event actor to match the original webhook actor.

  3. Bound candidate events to a trusted job-receipt or delivery timestamp so later activity cannot move the cutoff forward.

  4. Fail closed when more than one live event can satisfy the reconstruction criteria.

  5. Preserve the original webhook payload for every field that is intended to represent trigger-time state.

  6. Add regression tests for relabeling by another actor while the authorized job is queued or starting.

Actor matching alone is helpful but is not a complete event identity. The strongest design avoids reconstructing security state from a mutable history whenever the original delivery can provide or anchor that state.

Reviewing workflow YAML and trigger logic like this by hand does not scale across dozens of repositories. CodeAnt AI's AI code review flags exactly this class of authorization and lifecycle drift automatically, before a rebinding bug like this one ships in a GitHub Action.

Lessons for agentic CI systems

Authorization must cover the data snapshot

Approving an agentic job is not only a decision about who may start it. It is also a decision about which inputs the job is allowed to consume. If the input snapshot can move after approval, the authorization can be correct while the resulting action is not.

Semantic equality is not identity

Two events can share an action and a label name while belonging to different actors, times, and authorization contexts. Matching fields that look unique in ordinary use is not enough for a security boundary.

Lower roles should be tested against lifecycle transitions

Role checks are often reviewed in isolation. The more productive question is what a rejected role can change after a stronger role has passed the gate. Queues, retries, live lookups, reruns, and resumptions are all points where identities can be rebound.

Sink validation should avoid relying on hostile instructions

The strongest impact proof used a trusted maintainer task and attacker-controlled data. That design established the authorization failure without assuming that the model would follow an overt prompt injection.

This finding sits in the same "agentic tool trust boundaries" cluster as our Claude Code macOS TOCTOU writeup and our audit of a simple-git patch that turned into a remote code execution exploit: in all three, a control that is correct at the moment it runs is undermined by a later, mutable read of state it never pinned down. Anyone standing up label- or comment-triggered automation should also read our primer on what a CI/CD pipeline actually authorizes at each stage, since the same trigger-to-execution gap shows up well beyond Claude Code Action.

Disclosure timeline

Date

Event

August 5, 2026

Report submitted to Anthropic through HackerOne

August 13, 2026

Anthropic validated the issue and assigned High severity, CVSS 4.0 score 7.5

August 14, 2026

Bounty of $ XXXX awarded

August 24, 2026

Editorial cutoff; report remained triaged and private, with no confirmed fixed version

Conclusion

Claude Code Action correctly rejected the triage collaborator's own attempt to start a repository-writing run. The vulnerability arose because an already-authorized maintainer job later reconstructed its trigger boundary from mutable event history and accepted a newer event belonging to that rejected principal.

The central invariant is straightforward:

The event that defines a privileged agent's input boundary must be the same event that was authorized to start the run.

Preserving that identity prevents later repository activity from changing the meaning of an earlier authorization decision. It is the same discipline our agentic AI security testing looks for across every trigger surface, not only the ones GitHub happens to expose through a REST API.

For the rest of our agentic-tool disclosure work, see the CodeAnt AI security research hub.

References

  • Claude Code Action source

  • Pull request #1592: derive trigger timestamps for issue and pull request events

  • GitHub issue-event types

  • GitHub organization repository roles

  • GitHub collaborator permission API

  • CWE-863: Incorrect Authorization

This research was conducted through Anthropic's authorized HackerOne program. The reproduction used local Git repositories, synthetic credentials, and a loopback-only model fixture. No external repository or user data was modified.

FAQs

What did CodeAnt AI Security Research find in Claude Code Action?

What version was affected?

Isn't this just prompt injection?

Who can exploit it, and what do they gain?

Why is this rated High (7.5) rather than Critical?

Start Your 14-Day Free Trial

AI code reviews, security and quality trusted by modern engineering teams.

Table of Content
No headings found on page
Ship clean & secure code faster

Get Pentest Report

NO CC REQUIRED