What does SC2154 mean?
SC2154, "variable is referenced but not assigned", is ShellCheck's warning-level finding that a script reads a variable it never sets. Most of the time it is the other half of a typo: the assignment and the use are spelled differently, so the assignment is dead (SC2034) and the use is empty (SC2154). The rest of the time the variable really does come from outside, and ShellCheck is asking you to say so.
What actually breaks?
A backup script with a one-character mismatch between the assignment and the use. Run on this box with bash 5.3.9 and GNU tar 1.35.
$backup_dir expanded to nothing, so the archive path became /site-2026-09-01.tgz. As an unprivileged user that fails, which is the lucky outcome. Run from root's crontab, the way backup jobs usually are, tar writes the archive into /, the script prints "backup written to " and exits 0, and the backup directory stays empty until the day it is needed.
With set -u the script stops on the line that matters:
Which is why set -euo pipefail is on line 2 of every script on this site. ShellCheck reports the same thing without running anything.
What does ShellCheck say, and what is the fix?
The fix is to make the two names agree:
Where does SC2154 stay silent when it should not?
Uppercase names. ShellCheck treats every all-caps name as a possible environment variable:
No assignment, no warning. The lowercase twin, echo "$deploy_env", is reported. This is the single most common reason people believe SC2154 "does not work": the script uses UPPER_CASE names throughout, and the check never applies. set -u is the only guard for those.
A near-miss with an uppercase name is reported as SC2153 at info level instead. ARCHIVE_DIR assigned and $ARCHIVEDIR used produced SC2153 (info): Possible misspelling: ARCHIVEDIR may not be assigned. Did you mean ARCHIVE_DIR? on this box. Lowercase near-misses stay SC2154 and gain a hint: archivedir is referenced but not assigned (did you mean 'archive_dir'?), paired with an SC2034 on the assignment nobody reads.
Sourced files. source ./deploy.conf followed by a use of a variable the file sets is reported as SC1091, Not following: ./deploy.conf was not specified as input (see shellcheck -x), not SC2154. Add # shellcheck source=deploy.conf above the source line and run shellcheck -x and it follows the file.
Things ShellCheck already understands: read -r first second assigns both names, a variable assigned anywhere in the file counts even when the function that reads it appears earlier, and for x in ... assigns x.
How do I declare a variable that really comes from outside?
Both of these pass ShellCheck 0.11.0 without a directive and say what you mean:
The first is a no-op command whose only job is to expand the variable with :?, so an unset or empty value stops the script with your message before anything else runs:
Put the line near the top of the script, next to the other configuration, and the next reader knows the variable is an input rather than a typo.
When should I disable SC2154, and how?
When the value comes from a place ShellCheck cannot follow and you would rather not add the declaration line: a variable exported by a wrapper, or set by Environment= in a systemd unit. Say so:
For one run, shellcheck --exclude=SC2154 script.sh; for a project, disable=SC2154 in .shellcheckrc; to lint for this rule alone, shellcheck --include=SC2154 script.sh.
Related codes
- SC2034 — the assignment side of the same typo. See the SC2034 deep dive.
- SC2153 — the uppercase near-miss, at info level.
- SC1091 — a sourced file ShellCheck is not following.
- SC2155 —
local x=$(cmd)hides the command's exit status. See The Safe Bash Script Template.
Any other code: paste it into the ShellCheck Error Decoder.