Secrets not substituted in workflow - empty or masked output
Step logs show empty string where a secret should be
Focused incident mode — strips away everything but this runbook.
Symptoms
- Step logs show empty string where a secret should be
- Secret value appears as '***' in logs but the application receives an empty value
- Environment variable from secrets is not accessible in a shell step
GitHub Actions secrets resolve to empty string (not an error) when undefined. Always validate secrets exist before relying on them in production workflows.
Evidence to grab
Collect these artifacts before you change anything — they prove (or rule out) each cause.
Secrets used in the workflow
grep -n 'secrets\.' .github/workflows/<file>.ymlEnvironment variable mapping
cat .github/workflows/<file>.yml | grep -B2 -A5 'env:'
Diagnostic flow
Run the steps in order, marking each claim confirmed or refuted. The evidence you gather feeds the ranked causes below.
Guided decision tree is ready.
Steps with commands, expected output, and pass/fail branches.
Ranked root causes
Evidence checklist
- gh secret list does not show the secret name
- Secret was added to a different repository or environment
Confirm before fixing
gh secret list --repo <OWNER/REPO> --env <ENVIRONMENT> | grep SECRET_NAMECheck the environment-specific secret list
Fix
gh secret set SECRET_NAME -b '<VALUE>' --repo <OWNER/REPO>Create the missing secret
Resolution runbook
Apply from top to bottom after the cause is confirmed. Review every command before running it — risk tags are shown on each.
grep -n 'secrets\.' .github/workflows/<file>.ymlFind all secrets references
gh secret list --repo <OWNER/REPO>Check what secrets exist
gh secret set SECRET_NAME -b '<VALUE>' --repo <OWNER/REPO>Create or update the secret
Prevention
- Always define secrets in the repo settings before referencing them in workflows.
- Use 'environment:' on production jobs to scope secrets to the right environment.
- Add a validation step: if [ -z "$SECRET" ]; then echo "SECRET not set" && exit 1; fi.
- Use GitHub Actions built-in masking to prevent accidental log exposure.
Related runbooks
Same stack, same failure family — worth a skim.
Error: Resource not accessible by integration
Docker push to ghcr.io fails with 'denied: permission_denied' or '403 Forbidden'
Manually triggered workflow fails because input is empty
Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.