guide7 min readupdated

GitHub Actions secrets: the complete guide (2026)

How to add, use, and rotate secrets in GitHub Actions. Covers repo vs environment secrets, ${{ secrets.NAME }} syntax, masked logs, and OIDC as an alternative.

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

  1. Go to your repository → SettingsSecrets and variablesActions.
  2. Click New repository secret.
  3. Enter a name (e.g. DATABASE_URL) and paste the value.
  4. 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 deploy

You 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 list

Pulling 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.

Related tools

Continue reading