Claude Code GitHub Action Secret Exposure
Microsoft reported that the Claude Code GitHub Action could expose workflow secrets through a Read-tool path that reached /proc/self/environ; Anthropic shipped v2.1.128 as the fixed release.
On this page 0% read
Executive Summary
Microsoft described a secret-exposure case in the Claude Code GitHub Action where untrusted issue or pull-request content could reach the action’s Read tool and expose workflow secrets from the runner environment. Anthropic’s v2.1.128 release is the published fixed boundary, but the public report does not include exact repository or workflow examples, so broad exploitation should remain scoped as uncertain rather than assumed [1][2].
The practical defender takeaway is narrow: look for repositories that route untrusted issue or PR content into Claude Code jobs, confirm whether those jobs had access to ANTHROPIC_API_KEY or other workflow secrets, and treat any evidence of /proc/self/environ access as an exposure event until proven otherwise [1][2].
Key Facts
Threat Type: CI/CD secret exposure through an AI assistant workflow
Ecosystem: GitHub Actions
Registry: GitHub repositories
Affected Assets:
- Claude Code GitHub Action
- GitHub Actions runners
- CI/CD workflows processing untrusted issue or pull-request content
Malicious Versions:
- not publicly enumerated in the report
Known Good Versions:
- v2.1.128
Fixed Or Safe Versions:
- v2.1.128
Execution Trigger: workflow content routed through the Claude Code GitHub Action Read tool
Primary Impact: workflow secret exposure from the runner environment
Campaign Context: June 2026 agentic CI/CD incident with a public mitigation release but limited public exploitation detail.
Confidence: medium
Canonical Source: Microsoft security blog report and Anthropic release v2.1.128
Last Verified: 2026-06-05
Evidence Assessment
- confirmed: Microsoft reported the case and tied the exposure to a Read-tool path that could reach
/proc/self/environ[1]. - confirmed: Anthropic’s
v2.1.128release is the published fixed boundary in the supplied evidence set [2]. - unclear: The Microsoft article did not publish exact repository or workflow examples, so the full exploitation surface remains uncertain [1].
- unclear: Broader exploitation beyond the described Read-tool exposure path is not established in the public materials [1][2].
Impact Determination
| Classification | Criteria | Required evidence | Required action | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | The workflow or log export shows anthropics/claude-code, v2.1.128-boundary activity, secret material, or /proc/self/environ access. | Workflow file, job log, runner telemetry, and secret inventory. | Pause the workflow, preserve logs, and rotate any secret reachable from the job. | The exposure path is removed and downstream identity checks are clean. |
| Presumed exposed | The repository routes untrusted issue or PR content into Claude Code and had secrets available, but the exact runtime evidence is incomplete. | Workflow definition, job permissions, and secret availability. | Treat reachable secrets as exposed until you can rule out Read-tool access. | Owners verify rotation and no suspicious follow-on use. |
| Potentially exposed | The repo references Claude Code, but the specific run state, version, or secret availability is incomplete. | Repo search hits and workflow history. | Complete run-level scoping before classifying the asset. | Each matching workflow is dispositioned. |
| Not exposed | No Claude Code action reference, ANTHROPIC_API_KEY, or /proc/self/environ evidence exists in the scoped checkout or logs. | Negative checkout and log search results. | Keep the clean evidence set with the case record. | Search artifacts are preserved. |
| Unknown | Required checkout, workflow, or log telemetry is missing. | Named data gaps. | Keep the repository in the investigation queue. | Missing evidence is resolved or the owner accepts the gap. |
Minimum Evidence To Collect
- Workflow definitions that reference
anthropics/claude-codeor pinv2.1.128, because they establish whether the action was present in the execution path. - Job logs or exported runner telemetry that mention
ANTHROPIC_API_KEYor/proc/self/environ, because they show whether the secret-bearing environment was reachable. - Repository history for issue or pull-request handling workflows, because the report’s exposure path depends on untrusted content reaching the action [1][2].
Timeline
- 2026-05-04T23:01:47Z Anthropic published the
v2.1.128release boundary in the public release feed [2]. - 2026-06-05 Microsoft published the case writeup describing the Claude Code GitHub Action exposure path [1].
- 2026-06-05 This site-worker pass records the case as a standalone threat post with a matching local hunt.
What Happened
Microsoft’s report describes a workflow where the Claude Code GitHub Action processed untrusted issue or pull-request content and a Read tool path could expose the runner environment, including secrets such as ANTHROPIC_API_KEY [1]. The source material does not provide the exact repository or workflow examples, so defenders should treat the issue as a real exposure mechanism without assuming a wider, unproven mass-compromise campaign [1].
Anthropic’s v2.1.128 release gives the practical fixed boundary for responders who need a version marker during triage. For incident handling, the main question is not whether every Claude Code workflow was affected, but whether a specific checkout or log export shows the action, the fixed version boundary, and a path to /proc/self/environ [2].
Technical Analysis
Initial Access
The attack surface is not a package registry. It is a GitHub Actions workflow that accepts untrusted issue or pull-request content and hands that content to the Claude Code action, which means the relevant evidence is workflow logic, run history, and the secret set available to that job [1].
Package or Artifact Tampering
The public evidence supplied here points to a GitHub release boundary, not a malicious tarball. Use v2.1.128 as the version marker during scoping, and treat older references to the action as requiring additional review rather than immediate benign classification [2].
Execution Trigger
The trigger is workflow execution. If the runner invokes Claude Code on attacker-influenced content, then the action’s Read tool may traverse environment data that should not be available to untrusted input [1].
Payload Behavior
The publicly described behavior is secret exposure from the runner environment via /proc/self/environ, with ANTHROPIC_API_KEY serving as the clearest named secret class in the task context [1][2].
Exfiltration / C2
No public command-and-control endpoint was provided in the supplied sources, so the correct defender stance is to hunt for exposure evidence rather than assume a known remote sink [1][2].
Propagation
No propagation mechanism is established in the public evidence. The case should be treated as workflow-local exposure, not as a self-replicating campaign, unless local repository history shows otherwise [1][2].
Obfuscation or Evasion
The important evasion angle is contextual: a normal-looking assistant workflow can become risky when it is allowed to inspect environment state while processing untrusted content. That makes repository intent, not just file syntax, central to the review [1].
Affected Assets and Blast Radius
Affected Assets:
- ecosystems: GitHub Actions, CI/CD
- packages:
- versions: v2.1.128
- repositories: anthropics/claude-code
- ci_cd_systems: GitHub Actions runners, workflows processing untrusted issue or pull-request content
- container_images:
- developer_tools: Claude Code GitHub Action
Credentials At Risk:
ANTHROPIC_API_KEY- any other workflow secret injected into the runner environment
- downstream tokens reachable from a compromised GitHub Actions job
Not Currently Known To Affect:
- repositories that do not invoke the Claude Code GitHub Action
- workflows that never expose secret-bearing environment data to the assistant path
- broader exploitation beyond the public Microsoft-described case
Indicators of Compromise
The following indicators can be used to scope exposure across local repositories, workflow logs, and exported telemetry.
Files
.github/workflows/*.ymlaction.yml
Versions
v2.1.128
Secrets
ANTHROPIC_API_KEY
Paths
/proc/self/environ
Process Patterns
- Read tool access to
/proc/self/environ - workflow handling untrusted issue or pull-request content through Claude Code
Detection and Hunting
Hunt Manifest: claude-code-github-action-secret-exposure-hunt-1
- Title: checkout and exported log scope
- Question: Does the checkout or exported log scope contain indicators associated with the Claude Code GitHub Action secret-exposure case?
- Telemetry Family: process
- Telemetry Context: repository checkout or CI/CD log export
- Positive Signal: Matched anthropics/claude-code, v2.1.128, ANTHROPIC_API_KEY, or /proc/self/environ in checkout or log telemetry
- False Positives: Legitimate Claude Code release notes or examples may mention the action name or version without indicating exposure; require workflow context before escalation.
- Classification on Match: presumed_exposed
#!/usr/bin/env python3
"""Scan a checkout or exported log set for Claude Code GitHub Action exposure indicators.
The hunt focuses on the exact indicators reported in the case context:
- anthropics/claude-code
- v2.1.128
- ANTHROPIC_API_KEY
- /proc/self/environ
Exit codes:
0 = no indicators found
1 = at least one indicator matched
2 = usage or read error
"""
import argparse
import json
import sys
from pathlib import Path
INDICATORS = (
{
"id": "action_reference",
"category": "workflow",
"patterns": ("anthropics/claude-code", "uses: anthropics/claude-code"),
},
{
"id": "version_boundary",
"category": "release",
"patterns": ("v2.1.128", "2.1.128"),
},
{
"id": "workflow_secret",
"category": "secret",
"patterns": ("ANTHROPIC_API_KEY",),
},
{
"id": "readable_env_path",
"category": "runtime",
"patterns": ("/proc/self/environ",),
},
)
SKIP_DIR_NAMES = {".git", ".venv", "__pycache__", "dist", "build", "node_modules", "vendor", "coverage"}
MAX_TEXT_BYTES = 2 * 1024 * 1024
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"paths",
nargs="*",
default=["."],
help="One or more checkout or log-export paths to scan.",
)
parser.add_argument(
"--json-out",
type=Path,
help="Optional path to write structured findings as JSON.",
)
return parser.parse_args()
def is_probably_text(data: bytes) -> bool:
if not data:
return True
if b"\x00" in data:
return False
printable = sum(1 for byte in data[:4096] if byte in b"\t\n\r\x20" or 32 <= byte <= 126)
return printable / min(len(data), 4096) >= 0.75
def read_text(path: Path) -> str | None:
try:
data = path.read_bytes()
except OSError:
return None
if len(data) > MAX_TEXT_BYTES:
data = data[:MAX_TEXT_BYTES]
if not is_probably_text(data):
return None
for encoding in ("utf-8", "utf-8-sig", "latin-1"):
try:
return data.decode(encoding, errors="ignore")
except UnicodeDecodeError:
continue
return None
def iter_candidate_files(target: Path):
if target.is_file():
yield target
return
if not target.exists():
return
for child in target.rglob("*"):
if child.is_dir():
continue
if any(part in SKIP_DIR_NAMES for part in child.parts):
continue
yield child
def scan_text(text: str, file_path: Path):
findings = []
for indicator in INDICATORS:
for pattern in indicator["patterns"]:
start = 0
while True:
match_index = text.find(pattern, start)
if match_index < 0:
break
line_number = text.count("\n", 0, match_index) + 1
line_start = text.rfind("\n", 0, match_index) + 1
line_end = text.find("\n", match_index)
if line_end < 0:
line_end = len(text)
findings.append(
{
"category": indicator["category"],
"indicator_id": indicator["id"],
"pattern": pattern,
"file": str(file_path),
"line": line_number,
"excerpt": text[line_start:line_end].strip(),
}
)
start = match_index + len(pattern)
return findings
def scan_path(target: Path):
findings = []
for candidate in iter_candidate_files(target):
text = read_text(candidate)
if text is None:
continue
findings.extend(scan_text(text, candidate))
return findings
def main() -> int:
args = parse_args()
targets = [Path(path).expanduser().resolve() for path in args.paths]
all_findings = []
for target in targets:
if not target.exists():
print(f"[-] missing path: {target}", file=sys.stderr)
return 2
all_findings.extend(scan_path(target))
if args.json_out is not None:
args.json_out.parent.mkdir(parents=True, exist_ok=True)
args.json_out.write_text(json.dumps(all_findings, indent=2, sort_keys=True) + "\n", encoding="utf-8")
if all_findings:
print(f"[!] matched {len(all_findings)} indicator occurrence(s)")
for finding in all_findings:
print(
f"[MATCH] {finding['file']}:{finding['line']} "
f"{finding['indicator_id']} => {finding['pattern']}"
)
print(f" {finding['excerpt']}")
return 1
print("[+] no Claude Code GitHub Action exposure indicators found")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Sources
- Microsoft Security Blog: Securing CI/CD in an Agentic World — Claude Code GitHub Action case - Role: PRIMARY_RESEARCH - Impact: Public description of the Read-tool secret-exposure path and the uncertainty around exact repository examples.
- Anthropic Claude Code v2.1.128 release - Role: PRIMARY_RESEARCH - Impact: Fixed-release boundary used for scoping and remediation.