TL;DR
- Secrets are encrypted at rest and injected as environment variables into your workflow runners.
- Reference them with
${{ secrets.SECRET_NAME }}— they are automatically masked in logs. - Repo secrets are available to all workflows; environment secrets add an approval gate.
Adding a secret in the GitHub UI
- Go to your repository → Settings → Secrets and variables → Actions.
- Click New repository secret.
- Enter a name (e.g.
DATABASE_URL) and paste the value. - Click Add secret. The value is write-only — you cannot read it back.
To update a secret, click its name and enter a new value. The old value is overwritten immediately and all future workflow runs use the new value.
Using secrets in a workflow
Reference any secret with the ${{ secrets.NAME }} expression. GitHub injects it as an environment variable:
name: Deploy
on: push
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Run deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: npm run deployYou can also pass secrets directly to run steps via env: at the job level, or use them in with: inputs for actions.
Automatic masking
GitHub scans every log line and replaces any occurrence of a secret's value with ***. This works even if the secret is printed accidentally. However, masking is not foolproof — base64-encoded or URL-encoded versions of the secret will not be masked. Never deliberately print secrets in logs.
Repo secrets vs environment secrets
GitHub has two scopes for secrets:
- Repository secrets — available to all workflows in the repo. Good for CI credentials, test API keys, and deployment tokens that every PR branch needs.
- Environment secrets — scoped to a named environment (e.g.
production). Can require manual approval from a reviewer before the job runs. Use these for production deploy keys and live API credentials.
To use an environment secret, add environment: production to your job:
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment: production # triggers approval gate
steps:
- run: npm run deploy
env:
PROD_API_KEY: ${{ secrets.PROD_API_KEY }}Organisation secrets
If you manage multiple repos, organisation secrets let you define a secret once and share it across selected repos or all repos in the org. Go toOrganisation settings → Secrets and variables → Actions.
Avoid common mistakes
- Never hardcode secrets in workflow files. Even private repos get forked; secrets in YAML travel with the fork.
- Rotate on leak. If a secret appears in a log, rotate it immediately — GitHub's masking does not retroactively redact already-stored logs.
- Use OIDC for cloud credentials. Instead of storing AWS or GCP keys as secrets, configure OpenID Connect so GitHub authenticates directly to your cloud provider with short-lived tokens. No long-lived keys to rotate.
- Limit secret access on PRs from forks. Forked PRs do not receive secrets by default — this is correct behaviour. Never change the "Fork pull request workflows" setting to allow secrets on fork PRs.
Adding secrets via the CLI
# Set a secret
gh secret set DATABASE_URL --body "postgres://..." --repo owner/repo
# Set from a file
gh secret set PRIVATE_KEY < private.pem
# List secrets (names only — values are never shown)
gh secret listPulling secrets locally for development
GitHub Actions secrets are not accessible outside CI. For local development, use a .env.local file (gitignored) or a tool like vercel env pull / Doppler CLI to sync secrets to your machine. Never copy production secrets into a .env file that could be committed.