ShellCheck Error Codes Explained: How to Decode, Fix, and Prevent the Most Common Bash Warnings
Level: Beginner to Intermediate
Time: ~10 minutes
What you'll leave with: A clear understanding of ShellCheck's error code system, how to fix the ones that block deploys, and a free tool that decodes any SC code in seconds
The Problem Nobody Warns You About
ShellCheck is one of the best tools in the bash ecosystem. Every serious bash guide recommends it. CI pipelines run it automatically. VS Code underlines your scripts with it in real time.
Then it fires and you see this:
deploy.sh line 14: SC2086: Double quote to prevent globbing and word splitting.
deploy.sh line 22: SC2046: Quote this to prevent word splitting.
deploy.sh line 31: SC2034: unused_var appears unused. Verify use (or export if used externally).
And your CI pipeline is red.
You know something is wrong. You don't know what SC2086 actually means in the context of your specific script, which variable is the problem, or why it's a warning versus an error. So you Google it, open four tabs, find explanations that don't match your code, and burn 30+ minutes on a fix that should take 3.
This tutorial covers how ShellCheck's error code system works, what the most common codes actually mean, and how to fix them — with a free browser-based decoder tool that gives you before/after examples the moment you paste an SC code.
How ShellCheck's Error Code System Works
Every ShellCheck finding follows the same format:
filename line_number: SCxxxx: Short description of the problem.
The SC prefix stands for ShellCheck. The four-digit number identifies the specific rule. The short description is a hint — useful once you already know the rule, not very useful when you're seeing it for the first time.
The number prefix tells you the category:
SC1xxx — Parser errors and sourcing issues (syntax the shell can't read)
SC2xxx — Runtime warnings and semantic problems (syntax the shell can read but that will likely misbehave)
SC3xxx — Shell-specific portability issues (things that work in bash but not in sh or dash)
Severity runs in four levels:
| Badge | Severity | What it means |
error | Critical | Will break or produce wrong results at runtime |
warning | High | Will likely cause bugs in edge cases |
info | Medium | Worth reviewing — may cause problems |
⚪ style | Low | Correct but could be cleaner |
Fix error codes before the script ships. Address warning codes before they hit production data. Handle info and style codes during code review when you have time.
Rather than looking up each code individually, the ShellCheck Error Decoder at BashSnippets.xyz resolves any SC code instantly — directly in your browser.
Three input formats all work:
2086
SC2086
deploy.sh line 14: SC2086: Double quote to prevent globbing and word splitting.
Paste any of them. The tool detects the format automatically. If you paste a full ShellCheck output block with multiple SC codes, the tool detects all of them and gives you clickable buttons for each one — so you can work through a wall of warnings without reformatting anything.
Each result gives you:
- Plain English explanation of what's actually wrong
- Color-coded severity badge (error / warning / info / style)
- Before/After code block with a copy button on the fixed version
- "Why it matters" — the mechanism behind the bug, not just the fix
- The exact
# shellcheck disable=SCxxxx directive to suppress it when you've made a deliberate choice
There's also a category filter — click Quoting, Variables, Logic, Style, or Portability to browse all codes in that group at once. Useful when you're cleaning up a script and want to understand a whole class of issues instead of reacting to each warning individually.
The Most Common Codes — With Real Fixes
SC2086 — Unquoted Variable (Most Common of All)
This single code accounts for more ShellCheck output than the next four codes combined.
What fires it:
filename="my report.txt"
cat $filename
What's wrong: Without quotes, bash applies word splitting to $filename on every whitespace character, then applies glob expansion to any *, ?, or [ characters in the result. cat my report.txt becomes two arguments — my and report.txt — neither of which is the file you wanted. Add a glob character to the variable and the behavior becomes unpredictable. Add a * and rm $myvar becomes rm *.
The fix:
filename="my report.txt"
cat "$filename"
Two characters. Always quote your variables unless you have a specific reason not to and you understand exactly what will happen if you don't.
SC2046 — Unquoted Command Substitution
Same class of problem as SC2086, different form.
What fires it:
for f in $(ls /var/log); do
process $f
done
What's wrong: $(ls /var/log) expands to a space-separated string. Word splitting and glob expansion hit it the same way as an unquoted variable. Filenames with spaces get split into multiple tokens. The loop iterates over wrong values.
The fix:
for f in /var/log/*; do
process "$f"
done
Replace $(ls) with a glob — faster, safer, and handles spaces in filenames correctly. If you genuinely need command substitution, quote the whole thing: "$(some_command)".
SC2155 — Declare and Assign Separately
This one is subtle and causes real bugs in scripts with error handling.
What fires it:
local output=$(some_command)
What's wrong: local is a command and always exits 0. When you declare and assign on the same line, the exit code of some_command is swallowed — $? reflects local's success, not whether some_command actually worked. Under set -e, this silently bypasses your error handling.
The fix:
local output
output=$(some_command)
Now $? after the second line reflects whether some_command succeeded. Under set -e, a failure actually stops the script.
SC2164 — cd Without Checking if It Succeeded
This is how accidental mass deletions happen.
What fires it:
cd /var/backups
rm -f *.old
What's wrong: If cd fails — directory doesn't exist, permission denied, typo in the path — the script continues in whatever directory it was already in. Then rm -f *.old runs there instead.
The fix:
cd /var/backups || exit 1
rm -f *.old
If cd fails, exit immediately. You can also use || { echo "Cannot cd to /var/backups"; exit 1; } to add a message before exiting.
SC2034 — Variable Appears Unused
Usually a typo or leftover from a refactor.
What fires it:
logfile="/var/log/app.log"
echo "Writing to $log_file"
What's wrong: logfile is set, $log_file is used — they're different variable names. One of them is wrong.
The fix: Check the variable names match. If it's genuinely intentional (the variable gets exported for use in a subshell), add export logfile and ShellCheck will stop flagging it.
SC2006 — Backtick Command Substitution
A style issue, but one worth fixing for readability and nesting.
What fires it:
result=`some_command`
What's wrong: Backticks are the old form of command substitution. They work, but they can't nest cleanly — you need to escape inner backticks with backslashes, which gets ugly fast.
The fix:
result=$(some_command)
$() nests without escaping: $(outer $(inner)). Cleaner, more readable, universally supported.
SC2162 — read Without -r
What fires it:
read line
What's wrong: Without -r, read interprets backslashes in the input as escape sequences. \n in a config file becomes a newline. \\ becomes \. Your input gets silently mangled.
The fix:
read -r line
-r treats backslashes as literal characters. Use it on every read unless you specifically want escape interpretation.
SC2181 — Checking $? Instead of the Command Directly
What fires it:
some_command
if [ $? -ne 0 ]; then
echo "failed"
fi
What's wrong: The $? check is fragile — if any command runs between some_command and the if, $? reflects that command's exit code, not some_command's. It's also just harder to read.
The fix:
if ! some_command; then
echo "failed"
fi
Test the command directly in the if condition. Cleaner and immune to the $?-gets-overwritten bug.
How to Suppress a Warning You Disagree With
Sometimes ShellCheck fires on code that's intentionally written that way. You're not wrong — ShellCheck just doesn't have enough context to know you're not wrong.
The suppress directive goes on the line immediately above the flagged code:
# shellcheck disable=SC2086
echo $intentionally_unquoted
The decoder generates this directive for every code it covers — copy button included. Use suppression sparingly. When you suppress a warning, add a comment explaining why so the next person who reads the script understands it was deliberate:
# shellcheck disable=SC2086
# Word splitting intentional — IFS is set to newline, values won't contain spaces
for item in $list; do
Running ShellCheck Before It Runs You
The decoder is most useful when ShellCheck is already integrated into your workflow. Three practical setups:
Run manually before every commit:
shellcheck myscript.sh
# Check all shell scripts in the repo
find . -name "*.sh" -exec shellcheck {} \;
# Skip style warnings, focus on real bugs
shellcheck --severity=warning myscript.sh
VS Code: Install the "ShellCheck" extension by Timon Wong. Underlines issues in real time as you type. SC codes appear in the Problems panel — paste them straight into the decoder.
GitHub Actions CI:
name: ShellCheck
on: [push, pull_request]
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
This blocks PRs that introduce new ShellCheck errors. When a contributor's PR fails the check, they can paste the error code into the decoder to understand what they need to fix.
When ShellCheck Can't Help
ShellCheck is static analysis — it reads your code without running it. It catches structural problems, not runtime ones. It won't tell you:
- Whether the file your script expects actually exists
- Whether you have write permission to a directory
- Whether your cron expression runs when you think it does
- What exit code 141 means when your script dies unexpectedly
For those problems, the other tools at BashSnippets.xyz/tools are more relevant:
ShellCheck + these tools together cover the majority of "why is my bash script not doing what I want" scenarios.
Summary
ShellCheck is not the problem. The problem is that its error codes are opaque until you've seen each one a few times.
The pattern to fix:
- Run
shellcheck myscript.sh (or let your editor do it)
- Paste any SC code into the decoder
- Read the plain English explanation and severity badge
- Copy the fixed code from the Before/After block
- Use the disable directive when you've made a deliberate exception
Fix error and warning codes before you ship. Handle info and style codes during cleanup. Quote your variables. Check your cd calls. Declare and assign separately. That alone clears the majority of ShellCheck output on most scripts.
→ Open the ShellCheck Error Decoder
Free. No account. No email. Input focused on page load — paste your code and go.
BashSnippets is a free bash script library and interactive tools directory for Linux users, sysadmins, and developers. All tools are browser-based with no signup required. Scripts are on GitHub under MIT license.