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:
an authorization event, such as a maintainer applying a trusted label; and
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:
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.
Prefer an immutable event identifier supplied by a trusted delivery field when GitHub exposes one for the trigger.
If a live lookup is unavoidable, require the selected event actor to match the original webhook actor.
Bound candidate events to a trusted job-receipt or delivery timestamp so later activity cannot move the cutoff forward.
Fail closed when more than one live event can satisfy the reconstruction criteria.
Preserve the original webhook payload for every field that is intended to represent trigger-time state.
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.


