GitHub Actions Secret Not Found — Empty Environment Variable
Fix empty secrets in GitHub Actions by verifying the secret name, scoping it to the right environment, and handling fork and Dependabot restrictions.
Symptoms
- A step fails because ${{ secrets.TOKEN }} expands to an empty string
- The workflow works on push but not on pull requests from forks
- Authentication fails with a 401 although the secret is configured
Possible causes
- The secret name in the workflow differs from the configured name (case-sensitive)
- The secret is scoped to an environment the job does not declare
- Fork PRs and Dependabot do not receive repository secrets by default
Diagnosis steps
Confirm the secret exists and its scope
gh secret list --repo <owner>/<repo> && gh secret list --env productionInterpretation
Listing secrets per scope shows whether the name exists at the level your job uses.
Print only whether it is set, never the value
if [ -z "${{ secrets.TOKEN }}" ]; then echo 'TOKEN is empty'; fiInterpretation
This detects an empty expansion without leaking the value into logs.
Root cause
GitHub resolves secrets at runtime within their scope. A name mismatch, an environment-scoped secret used by a job without that environment, or a fork/Dependabot context all yield an empty value rather than an error, so downstream auth fails.
Fix
Match the secret name exactly, declare environment: production on jobs that use environment secrets, pass secrets explicitly to reusable workflows, and use pull_request_target with care or a separate token flow for fork PRs.
Risk note
pull_request_target with secrets exposes them to untrusted code; never run fork code with secrets in the same job.
Verify the fix
Rerun the job and confirm the secret is non-empty at the point of use; check that auth succeeds and no value appears in logs.
Symptoms
The error “GitHub Actions Secret Not Found — Empty Environment Variable” points at a specific failure mode. This guide documents the symptoms you can observe, the commands that isolate the cause, the root cause, the fix, and the checks that prove the fix works.
Observed symptoms include:
- A step fails because ${{ secrets.TOKEN }} expands to an empty string
- The workflow works on push but not on pull requests from forks
- Authentication fails with a 401 although the secret is configured
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Confirm the secret exists and its scope
gh secret list --repo <owner>/<repo> && gh secret list --env production
Listing secrets per scope shows whether the name exists at the level your job uses.
Step 2: Print only whether it is set, never the value
if [ -z "${{ secrets.TOKEN }}" ]; then echo 'TOKEN is empty'; fi
This detects an empty expansion without leaking the value into logs.
Likely contributors, in order of frequency:
- The secret name in the workflow differs from the configured name (case-sensitive)
- The secret is scoped to an environment the job does not declare
- Fork PRs and Dependabot do not receive repository secrets by default
Root Cause
GitHub resolves secrets at runtime within their scope. A name mismatch, an environment-scoped secret used by a job without that environment, or a fork/Dependabot context all yield an empty value rather than an error, so downstream auth fails.
The Fix
Match the secret name exactly, declare environment: production on jobs that use environment secrets, pass secrets explicitly to reusable workflows, and use pull_request_target with care or a separate token flow for fork PRs.
Watch out: pull_request_target with secrets exposes them to untrusted code; never run fork code with secrets in the same job.
jobs:
deploy:
environment: production # required to see environment secrets
steps:
- name: Guard against empty secret
run: |
if [ -z "${{ secrets.API_TOKEN }}" ]; then
echo '::error::API_TOKEN is empty'
exit 1
fi
# never echo the secret value itself
Verify and Prevent
Rerun the job and confirm the secret is non-empty at the point of use; check that auth succeeds and no value appears in logs.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.