ShellCheck Error Codes Explained: How to Decode, Fix, and Prevent the Most Common Bash Warnings

ShellCheck Error Codes Explained: How to Decode, Fix, and Prevent the Most Common Bash Warnings

Leader posted 8 min read

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.


The Free Decoder Tool

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:

  1. Run shellcheck myscript.sh (or let your editor do it)
  2. Paste any SC code into the decoder
  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

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.

Part 2 of 2 in Bash Snippets Tools

More Posts

What Is an Availability Zone Explained Simply

Ijay - Feb 12

Why most people quit AWS

Ijay - Feb 3

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

My Nginx Died at 2 AM and Nobody Noticed for 6 Hours. Now I Have a Watchdog Script

BashSnippets - May 21

Bash exit codes 0-255: what they mean and how to write the handler

BashSnippets - May 6
chevron_left

Commenters (This Week)

6 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!