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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:
```bash
# 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:**
```bash
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:**
```yaml
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](https://bashsnippets.xyz/tools/) are more relevant:
- **[Bash Exit Code Lookup](https://bashsnippets.xyz/tools/bash-exit-code-lookup.html)** — decode any exit code 0–255, get a generated error handler
- **[PATH Debugger](https://bashsnippets.xyz/tools/path-debugger.html)** — walk your `$PATH` and diagnose command-not-found errors
- **[Cron Job Builder](https://bashsnippets.xyz/tools/cron-job-builder.html)** — build cron syntax visually and preview the next 5 scheduled runs
- **[Chmod Permissions Builder](https://bashsnippets.xyz/tools/chmod-permissions-builder.html)** — convert permission checkboxes to the octal number
- **[Bash Boilerplate Generator](https://bashsnippets.xyz/tools/bash-boilerplate-generator.html)** — generate a production-ready script header with `set -euo pipefail`, logging, trap handlers, and more
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:
1. Run `shellcheck myscript.sh` (or let your editor do it)
2. Paste any SC code into the **[decoder](https://bashsnippets.xyz/tools/shellcheck-error-decoder.html)**
3. Read the plain English explanation and severity badge
4. Copy the fixed code from the Before/After block
5. 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](https://bashsnippets.xyz/tools/shellcheck-error-decoder.html)**
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](https://github.com/bashsnippets/bash-scripts) under MIT license.*