critical Threat analysis

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.

#github-actions#ci-cd#ai-assistants#credential-theft#workflow-secrets
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.128 release 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

    ClassificationCriteriaRequired evidenceRequired actionClosure condition
    Confirmed compromiseThe 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 exposedThe 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 exposedThe 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 exposedNo 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.
    UnknownRequired 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-code or pin v2.1.128, because they establish whether the action was present in the execution path.
    • Job logs or exported runner telemetry that mention ANTHROPIC_API_KEY or /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.128 release 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/*.yml
    • action.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: local repository and exported telemetry scope
    • Question: Does the telemetry scope contain patterns associated with Claude Code GitHub Action Secret Exposure?
    • Telemetry Family: process
    • Telemetry Context: host filesystem or log export
    • Positive Signal: Indicators of compromise matched in telemetry: local repository and exported telemetry scope
    #!/usr/bin/env python3
    """Generic IOC scope scanner for claude-code-github-action-secret-exposure.
    
    Searches repository trees and exported logs for literal IOC values from iocs.json.
    Exit codes:
      0: no matches
      1: one or more indicators matched
      2: execution error
    """
    import argparse
    import fnmatch
    import os
    import sys
    from pathlib import Path
    
    OUT = Path(os.environ.get("OUT", "hp-claude-code-github-action-secret-exposure-ioc-scope"))
    CONTENT_INDICATORS = [
      "anthropics/claude-code",
      "v2.1.128",
      "ANTHROPIC_API_KEY",
      "/proc/self/environ"
    ]
    PATH_INDICATORS = [
      ".github/workflows/*.yml",
      "action.yml"
    ]
    EXCLUDE_DIRS = {".git", "node_modules", "vendor", "dist", "build", ".venv", "__pycache__"}
    
    def _iter_files(root):
        root = Path(root)
        if not root.exists():
            return
        if root.is_file():
            yield root
            return
        for current, dirs, files in os.walk(root):
            dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
            for name in files:
                yield Path(current) / name
    
    def _path_matches(path):
        text = str(path)
        matches = []
        for indicator in PATH_INDICATORS:
            if not indicator:
                continue
            if indicator.startswith(("/", "~")):
                candidate = Path(os.path.expanduser(indicator))
                if candidate.exists() and path == candidate:
                    matches.append(indicator)
            if indicator in text or fnmatch.fnmatch(text, indicator) or fnmatch.fnmatch(path.name, indicator):
                matches.append(indicator)
        return matches
    
    def _content_matches(path):
        try:
            content = path.read_text(errors="ignore")
        except Exception:
            return []
        return [indicator for indicator in CONTENT_INDICATORS if indicator and indicator in content]
    
    def _scan_roots(roots):
        matches = []
        for root in roots:
            if not root:
                continue
            for path in _iter_files(root):
                for indicator in _path_matches(path):
                    matches.append(f"{path}: path matched {indicator!r}")
                for indicator in _content_matches(path):
                    matches.append(f"{path}: content matched {indicator!r}")
        return matches
    
    def main():
        parser = argparse.ArgumentParser(description="Scan files and logs for Halting Problems IOC values")
        parser.add_argument("roots", nargs="*", default=["."], help="File or directory roots to scan")
        parser.add_argument("--log-root", default=os.environ.get("LOG_ROOT", ""), help="Optional exported log directory")
        args = parser.parse_args()
    
        OUT.mkdir(parents=True, exist_ok=True)
        indicator_lines = sorted(set(CONTENT_INDICATORS + PATH_INDICATORS))
        (OUT / "ioc-indicators.txt").write_text("\n".join(indicator_lines) + "\n")
    
        roots = list(args.roots)
        if args.log_root:
            roots.append(args.log_root)
        matches = _scan_roots(roots)
        if matches:
            (OUT / "ioc-scope-matches.txt").write_text("\n".join(matches) + "\n")
            print(f"[!] Found {len(matches)} IOC matches; details written under {OUT}")
            return 1
        print(f"[+] No IOC matches found; indicator inventory written under {OUT}")
        return 0
    
    if __name__ == "__main__":
        try:
            sys.exit(main())
        except Exception as exc:
            print(f"[-] Execution failure: {exc}", file=sys.stderr)
            sys.exit(2)

    Hunt Manifest: claude-code-github-action-secret-exposure-hunt-2

    • Title: local repository and exported telemetry scope
    • Question: Does the telemetry scope contain patterns associated with Claude Code GitHub Action Secret Exposure?
    • Telemetry Family: process
    • Telemetry Context: host filesystem or log export
    • Positive Signal: Indicators of compromise matched in telemetry: local repository and exported telemetry scope
    #!/usr/bin/env python3
    """Generic IOC scope scanner for claude-code-github-action-secret-exposure.
    
    Searches repository trees and exported logs for literal IOC values from iocs.json.
    Exit codes:
      0: no matches
      1: one or more indicators matched
      2: execution error
    """
    import argparse
    import fnmatch
    import os
    import sys
    from pathlib import Path
    
    OUT = Path(os.environ.get("OUT", "hp-claude-code-github-action-secret-exposure-ioc-scope"))
    CONTENT_INDICATORS = [
      "anthropics/claude-code",
      "v2.1.128",
      "ANTHROPIC_API_KEY",
      "/proc/self/environ"
    ]
    PATH_INDICATORS = [
      ".github/workflows/*.yml",
      "action.yml"
    ]
    EXCLUDE_DIRS = {".git", "node_modules", "vendor", "dist", "build", ".venv", "__pycache__"}
    
    def _iter_files(root):
        root = Path(root)
        if not root.exists():
            return
        if root.is_file():
            yield root
            return
        for current, dirs, files in os.walk(root):
            dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
            for name in files:
                yield Path(current) / name
    
    def _path_matches(path):
        text = str(path)
        matches = []
        for indicator in PATH_INDICATORS:
            if not indicator:
                continue
            if indicator.startswith(("/", "~")):
                candidate = Path(os.path.expanduser(indicator))
                if candidate.exists() and path == candidate:
                    matches.append(indicator)
            if indicator in text or fnmatch.fnmatch(text, indicator) or fnmatch.fnmatch(path.name, indicator):
                matches.append(indicator)
        return matches
    
    def _content_matches(path):
        try:
            content = path.read_text(errors="ignore")
        except Exception:
            return []
        return [indicator for indicator in CONTENT_INDICATORS if indicator and indicator in content]
    
    def _scan_roots(roots):
        matches = []
        for root in roots:
            if not root:
                continue
            for path in _iter_files(root):
                for indicator in _path_matches(path):
                    matches.append(f"{path}: path matched {indicator!r}")
                for indicator in _content_matches(path):
                    matches.append(f"{path}: content matched {indicator!r}")
        return matches
    
    def main():
        parser = argparse.ArgumentParser(description="Scan files and logs for Halting Problems IOC values")
        parser.add_argument("roots", nargs="*", default=["."], help="File or directory roots to scan")
        parser.add_argument("--log-root", default=os.environ.get("LOG_ROOT", ""), help="Optional exported log directory")
        args = parser.parse_args()
    
        OUT.mkdir(parents=True, exist_ok=True)
        indicator_lines = sorted(set(CONTENT_INDICATORS + PATH_INDICATORS))
        (OUT / "ioc-indicators.txt").write_text("\n".join(indicator_lines) + "\n")
    
        roots = list(args.roots)
        if args.log_root:
            roots.append(args.log_root)
        matches = _scan_roots(roots)
        if matches:
            (OUT / "ioc-scope-matches.txt").write_text("\n".join(matches) + "\n")
            print(f"[!] Found {len(matches)} IOC matches; details written under {OUT}")
            return 1
        print(f"[+] No IOC matches found; indicator inventory written under {OUT}")
        return 0
    
    if __name__ == "__main__":
        try:
            sys.exit(main())
        except Exception as exc:
            print(f"[-] Execution failure: {exc}", file=sys.stderr)
            sys.exit(2)

    Sources

    1. 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.
    2. Anthropic Claude Code v2.1.128 release - Role: PRIMARY_RESEARCH - Impact: Fixed-release boundary used for scoping and remediation.