secrets: inherit not working in reusable workflows is two different failures wearing one name, and only one of them prints anything. Either the run is rejected before a job starts, which is loud and precise, or the called workflow reads an empty string and fails later on whatever it tried to authenticate to.

Inheritance is per hop, not per chain. Quoted from GitHub's reuse-workflows guide: "Secrets are only passed to directly called workflow".
What this error means
Start by asking whether any job ran at all. If the run has zero jobs and a startup failure, GitHub rejected the workflow before scheduling it, and the message names the secret and the side it is missing from. That is the loud shape, and it is what you get from explicit forwarding, never from inherit. If jobs did run and a step failed deep inside the called workflow with a 401, a blank token or an empty environment variable, that is the quiet shape: the expression ${{ secrets.NAME }} resolved to an empty string and GitHub printed nothing about it, because a secret that was never passed is not an error in an expression. The two have opposite fixes, so the first thing to read is the run's job count, not the step log.
```Startup failure, quoted from LedgerHQ/ledger-live#22146
Invalid secret, GH_BOT_CLIENT_ID is not defined in the referenced workflow.
Invalid secret, GH_BOT_PRIVATE_KEY is not defined in the referenced workflow.
## Common causes
### The caller forwards nothing at all
The called workflow references `secrets.SOMETHING` and the calling job has neither a `secrets:` map nor `secrets: inherit`. Nothing is rejected, because a caller is allowed to pass no secrets. The expression resolves to an empty string wherever it appears and the first tool that needs it fails with its own message.
### The caller switched to a named map and the callee never declared the names
This is the loud one, and it is what a move toward least privilege produces. GitHub validates that every name in the map is declared by the called workflow under `on.workflow_call.secrets`, and rejects the run with `Invalid secret, NAME is not defined in the referenced workflow.` The run ends as a startup failure with zero jobs, so there is no step log to open and local linters stay green because they do not resolve a remote reusable workflow.
### The secret is an environment secret, which no caller can forward
Subtle, silent, and the one that survives review. An environment secret cannot be passed from a caller at all, and the list of permitted keywords is why: "When you call a reusable workflow, you can only use the following keywords in the job containing the call", and `environment` is not on it. So the caller has no environment to inherit from, whichever forwarding style it uses. The key belongs on a job inside the called workflow, which then reads the secret itself.
### The chain is deeper than one call and a middle link forwards nothing
A > B > C where A inherits into B and B passes nothing into C. Both jobs run, so the run looks healthy, and only C is short a secret. In our experience this appears when a reusable workflow is refactored into two and the new middle layer is written by copying the old caller, which had no forwarding line because it did not need one.
## How to fix it
### Read the job count before the step log
1. Open the run. If it shows a startup failure and no jobs, the caller named a secret the callee does not declare, and the message names it.
2. If jobs ran and one step failed, the value arrived empty; the caller passed nothing, or passed something it did not itself have.
3. Only then open the workflow files, and open the one the run told you to.
> If the annotation reads `Invalid input` rather than `Invalid secret`, you have the adjacent failure: a key read as an input, usually a `secrets:` line indented into `with:`. See [Invalid input, secrets is not defined in the referenced workflow](https://latchkey.dev/learn/github-actions/gha-reusable-secret-not-declared).
### Declare the secrets the called workflow uses
Worth doing even if every caller uses `inherit` today, because it is what makes the explicit form possible and it documents the contract. A declaration marked `required: false` cannot change behavior for an existing caller under any forwarding style, so it is a safe addition to a shared workflow with callers you do not control.
```.github/workflows/deploy.yml, the called workflow (illustrative)
on:
workflow_call:
secrets:
DEPLOY_TOKEN:
required: false
APP_PRIVATE_KEY:
required: false
Forward at every level of a nested chain
Put a forwarding line on each calling job, not only the outermost one. inherit at every level is the blunt version and is fine inside one repository. Where a hop crosses into a repository you trust less, switch that hop to a named map and accept that the callee must declare the names.
Move the environment into the called workflow when the secret lives in one
If the value is stored on an environment, put the environment: key on the job inside the reusable workflow, never on the job that calls it. GitHub states the rule and its effect in one sentence: "If you include environment in the reusable workflow at the job level, the environment secret will be used, and not the secret passed from the caller workflow." If you want the secret without a deployment record against it, GitHub supports opting out of deployment creation while keeping the environment's secrets.
```.github/workflows/release.yml and deploy.yml (illustrative)
the caller: a job that uses uses: may not carry an environment: key
jobs:
call-deploy:
uses: ./.github/workflows/deploy.yml
secrets: inherit
deploy.yml, the called workflow: the environment goes on the job here
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
deployment: false
steps:
- run: ./deploy.sh
## How to prevent it
- Declare every secret a reusable workflow reads under `on.workflow_call.secrets`, so both calling styles work.
- Keep `inherit` for calls inside one repository and name the secrets when a call leaves it.
- Echo the length of a secret, never its value, in the first step of a called workflow while you are wiring one up.
- Treat a startup failure with zero jobs as a caller-and-callee mismatch, not as a broken runner.
## The two ways to pass a secret, and what each one costs
A reusable workflow receives nothing by default. There are exactly two ways to give it a secret and they place the burden in different places, which is why swapping one for the other breaks a working pipeline. Everything in this table is from GitHub's workflow syntax reference and its reuse-workflows guide.
| | `secrets: inherit` | A named `secrets:` map |
| --- | --- | --- |
| Caller writes | One line | One line per secret |
| Called workflow must declare it under `on.workflow_call.secrets` | No | Yes |
| What an undeclared name does | Nothing, it is still readable | Rejects the run at startup |
| What it sends | All secrets the caller has access to | Only the names you list |
| Survives a second hop on its own | No | No |
> The first row of that middle column is the sentence people miss. GitHub states it directly: "If the secrets are inherited by using `secrets: inherit` in the calling workflow, you can reference them even if they are not explicitly defined in the `on` key."
## What inherit actually inherits
The reference is one sentence and it is worth reading closely: "Use the `inherit` keyword to pass all the calling workflow's secrets to the called workflow. This includes all secrets the calling workflow has access to, namely organization, repository, and environment secrets. The `inherit` keyword can be used to pass secrets across repositories within the same organization, or across organizations within the same enterprise."
The load-bearing phrase is "the calling workflow has access to". `inherit` copies a set; it does not widen one, and for environment secrets that set is always empty. The same guide says why, in a warning: "Environment secrets cannot be passed from the caller workflow as `on.workflow_call` does not support the `environment` keyword. If you include `environment` in the reusable workflow at the job level, the environment secret will be used, and not the secret passed from the caller workflow." A job that calls a reusable workflow cannot carry an `environment:` key at all, so no forwarding style reaches an environment secret. The repair is on the other side of the call: environment secrets "are read when a job referencing the environment starts", and that job is the one inside the called workflow.
> That failure has its own page: [GitHub Actions environment secrets empty](https://latchkey.dev/learn/github-actions/gha-environment-secrets-not-loaded).
## Inheritance stops at the first hop
A chain of reusable workflows does not inherit transitively, and the guide says so: "Secrets are only passed to directly called workflow, so in the workflow chain A > B > C, workflow C will only receive secrets from A if they have been passed from A to B, and then from B to C." Its own worked example is a caller that writes `secrets: inherit # pass all secrets` into B, and B forwarding a single named secret into C. "Any of the other secrets passed to workflow B are not available to workflow C."
So `inherit` on the outermost caller and nothing on the middle one is the shape that breaks, and it breaks quietly: B runs, C runs, and one step inside C authenticates with an empty string. Every level has to forward, and a level that forwards by name has to have that name declared by the level below it.
```.github/workflows/a.yml and b.yml (illustrative)
jobs:
a-calls-b:
uses: ./.github/workflows/b.yml
secrets: inherit
# in b.yml, forwarding onward is a second decision
jobs:
b-calls-c:
uses: ./.github/workflows/c.yml
secrets: inherit
Why there is no recorded run on this page
The loud shape never reaches a runner: GitHub validates the caller against the called workflow and refuses to schedule the run, so there is no job and no log to record. The quiet shape does reach a runner, but what it produces is an empty string, and whatever a job does with an empty string afterwards is that tool's failure, not this one. Recording either would teach a reader nothing they cannot read from their own run's job count. The startup failure above is quoted from a public pull request, and the workflows here are illustrative.
FAQ
Does secrets: inherit require the called workflow to declare each secret?
No, and that is the whole difference between the two styles. GitHub states that when secrets are inherited with secrets: inherit, "you can reference them even if they are not explicitly defined in the on key". A named secrets: map is the opposite: every name in it must be declared under on.workflow_call.secrets or the run is rejected before any job starts.
Why do nested reusable workflows lose the inherited secrets?
Because inheritance is per call, not per chain. The guide is explicit: "Secrets are only passed to directly called workflow, so in the workflow chain A > B > C, workflow C will only receive secrets from A if they have been passed from A to B, and then from B to C." Add a forwarding line to the middle workflow and the chain works.
Does secrets: inherit pass environment secrets too?
In practice it cannot. The syntax reference lists "organization, repository, and environment secrets" among what inherit passes, but the reuse-workflows guide carves the last one out: "Environment secrets cannot be passed from the caller workflow as on.workflow_call does not support the environment keyword." A calling job has no way to name an environment, so put environment: on a job inside the called workflow and that job reads the secret itself.
Can secrets: inherit pass secrets to a workflow in another repository?
Yes, within limits GitHub names: "The inherit keyword can be used to pass secrets across repositories within the same organization, or across organizations within the same enterprise." It sends everything the caller has, so for a call that leaves your repository a named map is usually the better trade.
References
- GitHub Actions: workflow syntax, jobs..secrets.inherit
- GitHub Actions: reuse workflows, passing secrets to nested workflows
- GitHub Actions: secrets reference, when Actions reads secrets
- GitHub Actions: reusing workflow configurations, supported keywords for a calling job
- LedgerHQ/ledger-live#22146: a caller rejected for secrets the callee never declared
Top comments (0)