Use env for values scoped to a workflow, job or step, vars for non-secret configuration stored in GitHub, and secrets for anything sensitive. Pass untrusted data, such as a pull request title, through an intermediate environment variable, never by pasting an expression into a shell script, which invites script injection.
What kinds of variables does Actions have?
GitHub's reference lists three groups: default variables that GitHub sets, configuration variables you define in repository, organization or environment settings, and environment variables you define in the workflow file with env.
Default variables describe the run. GITHUB_REPOSITORY holds the owner and repository name, RUNNER_TEMP points to a temporary directory that is emptied at the start and end of each job, and GITHUB_REF identifies the triggering ref. The docs add that defaults are not available through the env context. Read them either as shell variables on the runner or through a matching context property, so GITHUB_REF corresponds to github.ref. You also cannot overwrite default values.
Where can you set env?
At the workflow level, the job level or the step level, with the narrowest scope winning. Prefer the narrowest scope that works. A token placed in workflow-level env is visible to every step, including third-party actions you added for a trivial task.
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build
env:
BUILD_MODE: release
run: echo "mode is $BUILD_MODE"
What are configuration variables for?
They hold non-secret settings such as a region name or a feature flag, kept in GitHub and read with the vars context. The documentation sets limits: each variable is capped at 48 KB, and you can store up to 1,000 organization variables, 500 per repository and 100 per environment, with a combined size limit for organization and repository variables.
Treat them as plain text. If a value would hurt you in a log or a screenshot, it belongs in secrets.
How does GITHUB_ENV work?
A step can set a variable for later steps by appending to the file whose path is in GITHUB_ENV. The docs describe it as the path on the runner to the file that sets variables from workflow commands, unique to each step. The variable becomes available to subsequent steps, not the current one.
The risk: if you write attacker-influenced text into that file, you may let the attacker set variables your later steps trust. Validate before writing, and avoid putting multi-line untrusted content there.
Why is script injection the real danger?
GitHub's secure use guidance explains that a workflow expression inside run: is substituted before the shell runs. Take a constructed step that checks a pull request title by inserting the expression directly into the script. A title containing a quote and a command ends the string and runs the command, with whatever permissions and secrets that job has.
The guidance recommends two safer shapes. The preferred one for inline scripts is an intermediate environment variable: set the expression as the value in env, then reference it as a normal shell variable inside the script. In the documentation example the hostile title is just stored in memory as data and the injection fails. The other is to use an action that takes the value as an argument. Also double-quote shell variables.
- name: Check PR title
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
if [[ "$TITLE" =~ ^feat ]]; then echo ok; fi
How do you keep secrets out of logs?
Pass secrets through env on the single step that needs them. For sensitive values generated at runtime that GitHub does not know about, the guidance says to mask them with the add-mask workflow command so they are redacted from logs. Redaction is done by the runner, so a secret is only scrubbed where that runner sees it. Rotate anything that leaked.
How to check or apply this
- Move
envblocks from workflow level down to the step that needs them. - Search workflows for expressions inside
run:that reference event data such as titles, branch names or comments, and route them through env. - Keep secrets in secrets, configuration in vars, and never mix them.
- Review every write to
GITHUB_ENVand validate what goes in. - Give the job token the least permissions it needs.
- Pin third-party actions to a full commit SHA and keep them updated.
- Mask runtime-generated sensitive values.
Where does Safeguard fit?
Safeguard's scanning can review workflow files and repositories for exposed secrets, and policy gates can block a pull request that introduces one. Review the injection patterns above by hand or with a linter as well.
Quick check
Are GitHub Actions variables secret?
No. Configuration variables are plain text. Use secrets for sensitive values and limit which steps receive them.
Why not put the PR title straight into the script?
Because the expression is substituted into the script text before it runs. Quote characters in the title can then change what the shell executes.
Sources
Last reviewed: 18 October 2026.