ShellCheck: Complete Configuration Guide
ShellCheck is a static analysis tool that finds bugs and suspicious patterns in shell scripts. Whether you write a one-liner in Bash or maintain thousands of lines of POSIX sh, ShellCheck catches common mistakes before they reach production. This guide walks through everything from installation to advanced configuration, with practical examples you can apply immediately.
What Is ShellCheck?
ShellCheck is an open-source linting tool written in Haskell. It parses shell scripts and reports issues ranging from quoting problems and unsafe variable expansion to deprecated syntax and portability concerns. It supports Bash, Dash, ksh, and other POSIX-compatible shells, and it integrates with virtually every major editor, CI system, and build tool.
Each finding is tagged with an identifier like SC2086, which you can look up on the ShellCheck wiki for a detailed explanation and recommended fix. This makes the tool both a linter and a learning resource.
Why ShellCheck Matters
Shell scripts are notoriously error-prone. Word splitting, glob expansion, and implicit type coercion can turn a simple command into a security incident. ShellCheck matters because it:
- Catches quoting bugs that cause word-splitting failures.
- Prevents command injection by flagging unquoted variables in dangerous positions.
- Improves portability across shells and operating systems.
- Enforces consistent style across a team or repository.
- Reduces debugging time by surfacing issues before execution.
Installing ShellCheck
ShellCheck is available through most package managers. Pick the one that matches your environment:
# Debian / Ubuntu
sudo apt install shellcheck
# macOS (Homebrew)
brew install shellcheck
# Arch Linux
sudo pacman -S shellcheck
# Fedora
sudo dnf install ShellCheck
# Using Cargo (Rust)
cargo install shellcheck-sarif
# Nix
nix-shell -p shellcheck
Verify the installation with shellcheck --version. If you need the latest features, download the precompiled binary from the GitHub releases page.
Basic Usage
The simplest invocation analyzes a single file and prints warnings to standard output:
shellcheck myscript.sh
You can also pipe scripts directly into ShellCheck, which is useful for inline checks:
echo 'echo $1' | shellcheck -
By default, ShellCheck reports all severity levels except informational. Use the --severity flag to filter:
shellcheck --severity=warning myscript.sh
Valid severity values are error, warning, info, and style.
Configuring ShellCheck with .shellcheckrc
ShellCheck reads configuration from a file named .shellcheckrc. It searches the current directory and each parent directory until it finds one, which makes it easy to scope rules per project. A typical configuration looks like this:
# .shellcheckrc
disable=SC2086,SC2155
enable=SC2034
external-sources=true
The directives are:
disable— comma-separated list of rules to suppress globally.enable— re-enable rules that are off by default.external-sources— when true, ShellCheck followssourcestatements to analyze included files.
Place this file at the root of your repository and commit it so every contributor gets the same ruleset.
Inline Directives
Sometimes a single line needs an exception. ShellCheck supports inline directives that override the global configuration for specific lines or blocks:
#!/usr/bin/env bash
# Disable SC2086 for the next line only
# shellcheck disable=SC2086
echo $USER
# Disable for an entire function
# shellcheck disable=SC2086
greet() {
echo "Hello, $1"
}
Use # shellcheck disable=SC2086 above a line to suppress a single warning, or above a function or block to suppress it for the whole scope. You can also add an explanatory comment for future readers:
# shellcheck disable=SC2086 # intentional word splitting
files=$(ls *.txt)
Specifying the Shell Dialect
ShellCheck infers the shell from the shebang line, but you can override it with --shell. This is essential when you lint scripts that are sourced rather than executed directly:
shellcheck --shell=bash lib/common.sh
Supported values include sh, bash, dash, ksh, and ash. Choosing the right dialect ensures ShellCheck applies the correct grammar and flags incompatible constructs.
Working with External Sources
When your script sources other files, ShellCheck cannot analyze them by default. Enable external-sources to follow those includes:
shellcheck --external-sources main.sh
Or set it permanently in .shellcheckrc:
external-sources=true
This is particularly useful in projects with shared library files, because ShellCheck can then validate variable usage and function definitions across the entire codebase.
Output Formats
ShellCheck supports multiple output formats for integration with other tools. The --format flag controls the style:
# Human-readable (default)
shellcheck --format=tty script.sh
# Checkstyle XML
shellcheck --format=checkstyle script.sh > report.xml
# JSON
shellcheck --format=json script.sh > report.json
# GCC-compatible
shellcheck --format=gcc script.sh
The JSON format is ideal for custom dashboards, while checkstyle integrates with Jenkins and SonarQube. The gcc format works well with editors that already parse compiler output.
Integrating with CI/CD
Running ShellCheck in CI ensures every pull request is linted. Here is a GitHub Actions workflow that fails the build on any warning:
name: Lint
on: [push, pull_request]
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck
run: shellcheck --severity=warning **/*.sh
For GitLab CI, add a similar job to your .gitlab-ci.yml:
shellcheck:
image: koalaman/shellcheck:stable
script:
- shellcheck --severity=warning $(find . -name '*.sh')
Using the official Docker image keeps your pipeline reproducible and avoids system package drift.
Editor Integration
Most editors have first-class ShellCheck support through plugins:
- VS Code — install the
timonwong.shellcheckextension. - Vim/Neovim — use
aleornvim-lint. - Emacs — use
flymake-shellcheckorflycheck. - JetBrains IDEs — enable the bundled Shell Script inspection.
These plugins read your .shellcheckrc automatically, so project-level configuration applies in the editor as well as in CI.
Best Practices
To get the most out of ShellCheck, follow these practices:
- Commit a
.shellcheckrcat the repository root so rules are consistent across the team. - Start with
--severity=warningand tighten tostyleonce the codebase is clean. - Avoid blanket disables; prefer inline directives with explanatory comments.
- Always set
--shellexplicitly when scripts are sourced rather than executed. - Enable
external-sourcesin projects that use shared library files. - Run ShellCheck in a pre-commit hook to catch issues before they reach version control.
- Treat ShellCheck output as educational — read the wiki entry for each code to understand the underlying issue.
A simple pre-commit hook using ShellCheck looks like this:
#!/usr/bin/env bash
# .git/hooks/pre-commit
set -e
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sh$')
if [ -n "$files" ]; then
shellcheck --severity=warning $files
fi
Make the hook executable with chmod +x .git/hooks/pre-commit and every commit will be linted automatically.
Common Warnings and How to Fix Them
Understanding the most frequent ShellCheck warnings helps you write better scripts from the start.
SC2086: Double quote to prevent globbing and word splitting.
# Bad
cp $file $destination
# Good
cp "$file" "$destination"
SC2155: Declare and assign separately to avoid masking return values.
# Bad
local var=$(some_command)
# Good
local var
var=$(some_command)
SC2046: Quote this to prevent word splitting.
# Bad
for f in $(ls); do echo "$f"; done
# Good
for f in *; do echo "$f"; done
SC2034: Variable appears unused.
# Bad
unused_var="hello"
# Good — remove it, or reference it
Each of these warnings points to a real class of bug. Fixing them improves robustness and readability.
Conclusion
ShellCheck is a small tool with an outsized impact on shell script quality. By combining a committed .shellcheckrc, targeted inline directives, explicit shell dialects, and CI integration, you create a safety net that catches mistakes early and teaches better scripting habits along the way. Start with sensible defaults, tighten the severity over time, and treat every warning as an opportunity to learn. Your scripts, your teammates, and your future self will all thank you.