
Shellcheck
- 90 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Shell script static analysis and linting. Use when linting shell, bash, sh scripts, analyzing shell errors, checking shell best practices, shell error codes.
About
Shell script static analysis and linting.. Use for shell/bash linting, script analysis, error checking, best practices verification.
- beginner skill
- core: code review & quality
Shellcheck by the numbers
- 90 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #468 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill shellcheckAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 90 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Shell script static analysis and linting. Use when linting shell, bash, sh scripts, analyzing shell errors, checking shell best practices, shell error codes.
Files
ShellCheck - Shell Script Static Analysis
Auto-routes when user mentions shellcheck, shell linting, bash script analysis, or SC error codes.
Overview
ShellCheck is a GPLv3-licensed static analysis tool that identifies bugs in bash/sh shell scripts. It detects:
- Syntax errors and parsing issues
- Semantic problems causing unexpected behavior
- Quoting issues and word splitting bugs
- POSIX compatibility warnings
- Style and best practice violations
Voice Notification
When executing a workflow, do BOTH:
1. Send voice notification:
curl -s -X POST http://localhost:8888/notify \
-H "Content-Type: application/json" \
-d '{"message": "Running the WORKFLOWNAME workflow from the ShellCheck skill"}' \
> /dev/null 2>&1 &2. Output text notification:
Running the **WorkflowName** workflow from the **ShellCheck** skill...Workflow Routing
| Workflow | Trigger | File |
|---|---|---|
| Analyze | "shellcheck this", "lint script", "check shell" | Workflows/Analyze.md |
| Fix | "fix shell errors", "apply shellcheck fixes" | Workflows/Fix.md |
| Setup | "setup shellcheck", "configure shellcheck" | Workflows/Setup.md |
| Explain | "explain SC2086", "what is SC code" | Workflows/Explain.md |
Quick Reference
Basic Usage
# Check a script
shellcheck myscript.sh
# Specify shell dialect
shellcheck -s bash myscript.sh
# Exclude specific codes
shellcheck -e SC2086,SC2046 myscript.sh
# Output formats
shellcheck -f gcc myscript.sh # Editor integration
shellcheck -f json myscript.sh # Machine readable
shellcheck -f diff myscript.sh # Auto-fix patchesCommon SC Codes
| Code | Issue | Fix |
|---|---|---|
| SC2086 | Unquoted variable | "$var" |
| SC2046 | Unquoted command substitution | "$(cmd)" |
| SC2034 | Unused variable | Remove or export |
| SC2154 | Unassigned variable | Assign or disable |
| SC2155 | Declare and assign separately | Split declaration |
Inline Directives
# Disable for next command
# shellcheck disable=SC2086
echo $var
# Disable for entire file (after shebang)
#!/bin/bash
# shellcheck disable=SC2086,SC2046Full Documentation
- Error Codes:
SkillSearch('shellcheck error codes')-> loads ErrorCodes.md - Configuration:
SkillSearch('shellcheck config')-> loads Configuration.md - CI/CD Integration:
SkillSearch('shellcheck ci')-> loads Integration.md - Best Practices:
SkillSearch('shellcheck practices')-> loads BestPractices.md
Examples
Example 1: Analyze a script
User: "shellcheck my deploy script"
-> Invokes Analyze workflow
-> Runs shellcheck with JSON output
-> Presents findings grouped by severity
-> Suggests fixes with wiki linksExample 2: Fix common issues
User: "fix the shellcheck errors in scripts/"
-> Invokes Fix workflow
-> Generates diff output
-> Applies fixes interactively
-> Re-runs validationExample 3: Setup for project
User: "setup shellcheck for this repo"
-> Invokes Setup workflow
-> Creates .shellcheckrc
-> Adds pre-commit hook
-> Configures CI workflowExample 4: Explain an error code
User: "what does SC2086 mean?"
-> Invokes Explain workflow
-> Fetches wiki documentation
-> Shows examples and fixes
-> Provides context-specific guidance---
Gotchas
- SC2086 is wrong inside `[[ ]]`: Bash's
[[ ]]does not word-split, so[[ -n $var ]]is safe unquoted. Disabling SC2086 on[[ ]]blocks is a sign you're applying the lint to the wrong construct, not a sign the rule is broken. - SC2034 fires on indirectly-used variables: Variables consumed via
${!prefix*}indirection,declare -pintrospection, or sourced into another script trigger "unused" false positives. Use# shellcheck disable=SC2034with a comment explaining the indirection — don't silence globally. - Shebang determines the dialect, not the filename:
script.shwith#!/bin/shis checked as POSIX sh and rejects bashisms like[[ ]]or arrays. Either set the correct shebang or pass-s bashexplicitly; never rely on the.shextension. - `shellcheck -e SC2086,SC2046` in `.shellcheckrc` hides real bugs: Project-wide disables compound — a year later nobody remembers why and unquoted expansions ship to prod. Prefer inline disables with a justification comment over global suppression.
- Source-following requires `-x` flag:
source ./lib.shis not analyzed by default. Run withshellcheck -x script.shfor full coverage, or add# shellcheck source=./lib.shdirectives. CI configs frequently miss this and ship un-linted sourced files. - `-f diff` patches assume the script parses cleanly: Syntax errors prevent the auto-fix output entirely, with no clear message. If
-f diffproduces nothing, run without-ffirst and fix parse errors before re-running for patches.
ShellCheck Best Practices
Comprehensive guide to writing clean, maintainable shell scripts.
The Top 10 Rules
1. Always Quote Variables
# Bad - word splitting and globbing
echo $filename
rm $file
# Good - prevents issues
echo "$filename"
rm "$file"
# Exception: inside [[ ]] for pattern matching
[[ $string == pattern* ]]2. Use $() Instead of Backticks
# Bad - hard to nest, hard to read
files=`ls`
date=`date +%Y-\`date +%m\``
# Good - clear nesting
files=$(ls)
date=$(date +%Y-$(date +%m))3. Use [[ Instead of [ in Bash
# Bad - quoting required, no pattern matching
[ "$var" = "value" ]
[ -n "$var" -a -f "$file" ]
# Good - safer, more features
[[ $var == "value" ]]
[[ -n $var && -f $file ]]
[[ $string =~ ^[0-9]+$ ]] # Regex support4. Use set -euo pipefail
#!/bin/bash
set -euo pipefail
# -e: Exit on error
# -u: Error on unset variables
# -o pipefail: Pipeline fails if any command fails5. Declare and Assign Separately
# Bad - masks return value
local output=$(command_that_might_fail)
# Good - captures return value
local output
output=$(command_that_might_fail) || handle_error6. Use Arrays for Lists
# Bad - word splitting issues
files="file1.txt file2.txt file with spaces.txt"
for f in $files; do
process "$f"
done
# Good - proper array handling
files=("file1.txt" "file2.txt" "file with spaces.txt")
for f in "${files[@]}"; do
process "$f"
done7. Use "$@" for Arguments
# Bad - loses quoting
wrapper() {
command $@
}
# Good - preserves quoting
wrapper() {
command "$@"
}8. Handle Errors Explicitly
# Bad - continues on failure
cd /some/directory
rm -rf *
# Good - explicit error handling
cd /some/directory || { echo "cd failed" >&2; exit 1; }
rm -rf ./*
# Or with trap
trap 'echo "Error on line $LINENO" >&2' ERR9. Use command -v Instead of which
# Bad - non-standard, varies by system
if which git > /dev/null; then
...
fi
# Good - POSIX compliant
if command -v git > /dev/null 2>&1; then
...
fi10. Use Parameter Expansion
# Bad - external commands
filename=$(basename "$path")
dirname=$(dirname "$path")
extension=$(echo "$file" | sed 's/.*\.//')
# Good - built-in parameter expansion
filename=${path##*/}
dirname=${path%/*}
extension=${file##*.}Common Anti-Patterns
Parsing ls Output
# Bad - breaks on special characters
for file in $(ls); do
process "$file"
done
# Good - use globs
for file in *; do
[[ -f $file ]] && process "$file"
done
# Or find with -print0
while IFS= read -r -d '' file; do
process "$file"
done < <(find . -type f -print0)Using cat Unnecessarily
# Bad - useless use of cat (UUOC)
cat file | grep pattern
cat file | wc -l
# Good - direct input
grep pattern file
wc -l < fileTesting with $?
# Bad - indirect check
command
if [ $? -eq 0 ]; then
echo "success"
fi
# Good - direct check
if command; then
echo "success"
fiUnquoted Variables in Tests
# Bad - breaks if empty
if [ -n $var ]; then
...
fi
# Good - properly quoted
if [[ -n $var ]]; then
...
fiRead Without -r
# Bad - backslashes interpreted
while read line; do
echo "$line"
done < file
# Good - raw read
while IFS= read -r line; do
echo "$line"
done < fileScript Template
#!/usr/bin/env bash
#
# Script: myscript.sh
# Description: Brief description
# Usage: myscript.sh [options] <arguments>
#
set -euo pipefail
# Constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# Default values
VERBOSE=${VERBOSE:-false}
DRY_RUN=${DRY_RUN:-false}
# Colors (if terminal)
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
NC=''
fi
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
die() { log_error "$*"; exit 1; }
# Usage/help
usage() {
cat << EOF
Usage: $SCRIPT_NAME [options] <arguments>
Description of what this script does.
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-n, --dry-run Show what would be done
Examples:
$SCRIPT_NAME --verbose /path/to/file
$SCRIPT_NAME -n
EOF
}
# Parse arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--verbose)
VERBOSE=true
shift
;;
-n|--dry-run)
DRY_RUN=true
shift
;;
--)
shift
break
;;
-*)
die "Unknown option: $1"
;;
*)
break
;;
esac
done
# Remaining arguments
ARGS=("$@")
}
# Cleanup function
cleanup() {
# Remove temp files, restore state, etc.
:
}
# Main function
main() {
trap cleanup EXIT
parse_args "$@"
[[ $VERBOSE == true ]] && log_info "Verbose mode enabled"
[[ $DRY_RUN == true ]] && log_warn "Dry-run mode - no changes will be made"
# Your main logic here
if [[ ${#ARGS[@]} -eq 0 ]]; then
die "No arguments provided"
fi
for arg in "${ARGS[@]}"; do
log_info "Processing: $arg"
done
}
main "$@"Quoting Reference
| Context | Quote? | Example |
|---|---|---|
| Variable assignment | No | var=$other |
| Variable in string | Yes | echo "Hello $name" |
| Variable as argument | Yes | command "$var" |
| Array element | Yes | "${array[0]}" |
| All array elements | Yes | "${array[@]}" |
| Command substitution | Yes | var="$(command)" |
| Arithmetic | No | $((x + 1)) |
Inside [[ ]] | Usually no | [[ $var == pattern ]] |
Inside [ ] | Yes | [ "$var" = "value" ] |
| Glob patterns | No | for f in *.txt |
Parameter Expansion Cheat Sheet
| Expansion | Description |
|---|---|
${var:-default} | Use default if unset/null |
${var:=default} | Set to default if unset/null |
${var:?error} | Error if unset/null |
${var:+alt} | Use alt if set |
${#var} | String length |
${var#pattern} | Remove shortest prefix match |
${var##pattern} | Remove longest prefix match |
${var%pattern} | Remove shortest suffix match |
${var%%pattern} | Remove longest suffix match |
${var/pat/rep} | Replace first match |
${var//pat/rep} | Replace all matches |
${var^} | Uppercase first char |
${var^^} | Uppercase all |
${var,} | Lowercase first char |
${var,,} | Lowercase all |
${var:pos:len} | Substring |
Test Operators Cheat Sheet
File Tests
| Test | Description |
|---|---|
-e file | Exists |
-f file | Regular file |
-d file | Directory |
-L file | Symbolic link |
-r file | Readable |
-w file | Writable |
-x file | Executable |
-s file | Size > 0 |
f1 -nt f2 | f1 newer than f2 |
f1 -ot f2 | f1 older than f2 |
String Tests
| Test | Description |
|---|---|
-z string | Empty |
-n string | Not empty |
s1 = s2 | Equal |
s1 != s2 | Not equal |
s1 < s2 | Less than (lexical) |
s1 > s2 | Greater than (lexical) |
Numeric Tests
| Test | Description |
|---|---|
n1 -eq n2 | Equal |
n1 -ne n2 | Not equal |
n1 -lt n2 | Less than |
n1 -le n2 | Less or equal |
n1 -gt n2 | Greater than |
n1 -ge n2 | Greater or equal |
ShellCheck Workflow
Development Phase
1. Write code with editor integration (real-time feedback) 2. Run shellcheck before committing 3. Fix issues or add justified suppressions
CI Phase
1. Pre-commit hook catches issues before push 2. CI pipeline enforces standards 3. Quality gates prevent merging broken scripts
Suppression Guidelines
Only suppress when:
- False positive (rare)
- Intentional behavior (document why)
- External variable (sourced from elsewhere)
Always add a comment:
# shellcheck disable=SC2034 # Variable used by sourced scripts
readonly CONFIG_PATH="/etc/myapp"
# Intentionally unquoted for word splitting
# shellcheck disable=SC2086
flags=$USER_FLAGSResources
ShellCheck Configuration Reference
Complete guide to configuring ShellCheck behavior.
Configuration Methods
ShellCheck can be configured through multiple methods (in order of precedence):
1. Command-line flags - Highest precedence 2. Inline directives - Per-command or file-wide 3. Configuration file (.shellcheckrc) - Project or user-wide 4. Environment variable (SHELLCHECK_OPTS) - System-wide defaults
Configuration File (.shellcheckrc)
File Locations
ShellCheck searches for configuration in this order:
1. Script's directory - .shellcheckrc or shellcheckrc 2. Parent directories - Walking up to root 3. Home directory - ~/.shellcheckrc 4. XDG config - ~/.config/shellcheckrc (Unix) 5. AppData - %APPDATA%/shellcheckrc (Windows)
Only the first file found is used.
File Format
# Comments start with #
key=value
key=value with spaces
key="quoted value"
key='single quoted'Example .shellcheckrc
# Project-wide ShellCheck configuration
# Shell dialect (sh, bash, dash, ksh, busybox)
shell=bash
# Source file handling
source-path=SCRIPTDIR
source-path=SCRIPTDIR/lib
source-path=/usr/local/lib/myproject
# Allow following source statements
external-sources=true
# Enable extended dataflow analysis
extended-analysis=true
# Enable optional checks
enable=quote-safe-variables
enable=check-unassigned-uppercase
enable=require-variable-braces
enable=deprecate-which
# Disable specific warnings
disable=SC2059 # printf format string variable
disable=SC2034 # Unused variable (false positives)
disable=SC1090 # Can't follow non-constant source
disable=SC1091 # Not following sourced fileMinimal Project .shellcheckrc
# Minimum recommended configuration
shell=bash
source-path=SCRIPTDIR
external-sources=true
enable=deprecate-whichInline Directives
Syntax
# shellcheck key=valuePlacement Rules
| Placement | Scope |
|---|---|
| After shebang | Entire file |
| Before command | Next complete command only |
| Before function | Entire function |
| Before loop/if | Entire block |
Available Directive Keys
disable
Suppress specific warnings:
# Single code
# shellcheck disable=SC2086
echo $var
# Multiple codes
# shellcheck disable=SC2086,SC2046
files=$(ls *.txt)
# Range of codes
# shellcheck disable=SC1090-SC1100
# All warnings (v0.8+)
# shellcheck disable=allenable
Activate optional checks:
#!/bin/bash
# shellcheck enable=require-variable-braces
# shellcheck enable=quote-safe-variablesshell
Specify shell dialect:
#!/bin/sh
# shellcheck shell=bash
# Script uses bash features despite sh shebangsource
Override sourced file location:
# Skip sourcing (use /dev/null)
# shellcheck source=/dev/null
source "$CONFIG_FILE"
# Specify actual location
# shellcheck source=./lib/common.sh
source "$LIB_DIR/common.sh"
# Use script directory
# shellcheck source=SCRIPTDIR/lib/utils.sh
source "$(dirname "$0")/lib/utils.sh"source-path
Add search paths for sourced files:
# shellcheck source-path=SCRIPTDIR
# shellcheck source-path=SCRIPTDIR/../lib
# shellcheck source-path=/usr/local/libexternal-sources
Allow following arbitrary source statements:
# Can ONLY be set in .shellcheckrc, not inline
external-sources=trueextended-analysis
Toggle dataflow analysis (for performance on large files):
# Disable for auto-generated scripts
# shellcheck extended-analysis=falseCommand-Line Options
Analysis Options
# Specify shell dialect
shellcheck -s bash script.sh
shellcheck --shell=bash script.sh
# Include sourced file warnings
shellcheck -a script.sh
shellcheck --check-sourced script.sh
# Follow external sources
shellcheck -x script.sh
shellcheck --external-sources script.sh
# Source search paths
shellcheck -P ./lib:./src script.sh
shellcheck --source-path=./lib:./src script.shFiltering Options
# Exclude codes
shellcheck -e SC2086,SC2046 script.sh
shellcheck --exclude=SC2086,SC2046 script.sh
# Include only specific codes
shellcheck -i SC2086 script.sh
shellcheck --include=SC2086 script.sh
# Enable optional checks
shellcheck -o require-variable-braces script.sh
shellcheck --enable=require-variable-braces script.sh
shellcheck -o all script.sh # Enable all optional
# Minimum severity
shellcheck -S error script.sh # Only errors
shellcheck -S warning script.sh # Errors + warnings
shellcheck -S info script.sh # Errors + warnings + info
shellcheck -S style script.sh # All (default)
shellcheck --severity=warning script.shOutput Options
# Output format
shellcheck -f tty script.sh # Human-readable (default)
shellcheck -f gcc script.sh # GCC format for editors
shellcheck -f checkstyle script.sh # XML for CI tools
shellcheck -f diff script.sh # Unified diff for auto-fix
shellcheck -f json script.sh # JSON (legacy)
shellcheck -f json1 script.sh # JSON (compact)
shellcheck -f quiet script.sh # Exit code only
shellcheck --format=gcc script.sh
# Color control
shellcheck -C always script.sh
shellcheck -C never script.sh
shellcheck -C auto script.sh
shellcheck --color=always script.sh
# Wiki links
shellcheck -W 3 script.sh # Show 3 wiki links
shellcheck -W 0 script.sh # Disable wiki links
shellcheck --wiki-link-count=3 script.shConfiguration Options
# Skip .shellcheckrc
shellcheck --norc script.sh
# Use specific config file
shellcheck --rcfile=./custom.shellcheckrc script.sh
# Extended analysis control
shellcheck --extended-analysis=true script.sh
shellcheck --extended-analysis=false script.sh
# List optional checks
shellcheck --list-optionalEnvironment Variable
Set default options via SHELLCHECK_OPTS:
# In ~/.bashrc or ~/.zshrc
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016 --enable=deprecate-which'
# One-time override
SHELLCHECK_OPTS='-x -S warning' shellcheck script.shOptional Checks
List available with shellcheck --list-optional:
| Check Name | Description |
|---|---|
add-default-case | Suggest default *) in case statements |
avoid-negated-conditions | Suggest removing unnecessary negations |
avoid-nullary-conditions | Explicitly use -n in conditions |
check-extra-masked-returns | More masked return value checks |
check-set-e-suppressed | Warn when set -e is suppressed |
check-unassigned-uppercase | Warn about uninitialized uppercase vars |
deprecate-which | Suggest command -v over which |
quote-safe-variables | Suggest quoting even safe variables |
require-double-brackets | Require [[ over [ in Bash |
require-variable-braces | Require ${var} over $var |
Enabling Optional Checks
# Command line
shellcheck -o deprecate-which,require-variable-braces script.sh
shellcheck -o all script.sh # Enable all
# .shellcheckrc
enable=deprecate-which
enable=require-variable-braces
# Inline (file-wide only)
#!/bin/bash
# shellcheck enable=require-variable-bracesPlatform-Specific Notes
Snap Users
The Snap sandbox blocks hidden files. Use shellcheckrc (no dot):
# Instead of .shellcheckrc
mv .shellcheckrc shellcheckrcDocker Users
Mount config files explicitly:
docker run --rm \
-v "$PWD:/mnt" \
-v "$HOME/.shellcheckrc:/root/.shellcheckrc:ro" \
koalaman/shellcheck /mnt/script.shOr use environment variable:
docker run --rm \
-e SHELLCHECK_OPTS='--shell=bash' \
-v "$PWD:/mnt" \
koalaman/shellcheck /mnt/script.shShellCheck Error Codes Reference
Complete reference for ShellCheck diagnostic codes (SC codes).
Code Categories
SC1xxx - Parser and Syntax Errors
Fundamental parsing issues and syntax violations.
| Code | Description |
|---|---|
| SC1000 | $ is not used specially and should be escaped |
| SC1001 | This \o will be a regular 'o' in this context |
| SC1007 | Remove space after = if trying to assign a value |
| SC1008 | Unrecognized shebang |
| SC1009 | Mentioned parser error was in this... |
| SC1010 | Use semicolon or linefeed before done (or quote to make literal) |
| SC1012 | \t is just literal 't' here. Use printf for escape sequences |
| SC1014 | Use if cmd; then .. or if $(cmd) instead |
| SC1015 | This is a unicode double quote. Use ASCII double quotes |
| SC1016 | This is a unicode single quote. Use ASCII single quotes |
| SC1018 | This is a unicode bullet point. Use ASCII periods |
| SC1033 | Mismatched brackets - expected [[ but found [ |
| SC1034 | Mismatched brackets - expected [ but found [[ |
| SC1035 | You need a space here |
| SC1036 | Expected ) but found end of file |
| SC1037 | Braces required for positionals over 9, e.g. ${10} |
| SC1039 | Expected here-doc line (missing terminator) |
| SC1040 | When using here-doc with quotes, no parameter expansion |
| SC1041 | Found do but expected - |
| SC1042 | Found do where expected select/for/while |
| SC1043 | This seems like an ended here-doc |
| SC1044 | Couldn't find end of here-doc |
| SC1045 | It's not foo &; bar, just foo & bar |
| SC1046 | Couldn't find fi for this if |
| SC1047 | Expected fi to close if statement |
| SC1048 | Can't have empty then clause |
| SC1049 | Expected do |
| SC1058 | Expected do |
| SC1060 | Expected do |
| SC1061 | Couldn't find done for this do |
| SC1062 | Expected done to close do |
| SC1064 | Expected { to open function definition |
| SC1065 | Trying to define function with arguments? |
| SC1071 | ShellCheck only supports sh/bash/dash/ksh |
| SC1072 | Expected single semicolon in arithmetic for loop |
| SC1073 | Couldn't parse arithmetic expression |
| SC1078 | Did you forget to close this double-quoted string? |
| SC1079 | Missing closing brace for this } |
| SC1081 | Scripts are case sensitive (or try =~ for regex) |
| SC1083 | { is literal here. Use \{ to escape or ${ to start |
| SC1090 | Can't follow non-constant source. Use directive |
| SC1091 | Not following sourced file |
| SC1094 | Parsing of sourced file failed |
| SC1095 | Use #!/bin/bash instead of #!bin/bash |
SC2xxx - Semantic and Style Issues
Logic errors, performance concerns, and best practices.
Quoting Issues
| Code | Description |
|---|---|
| SC2001 | See if you can use ${variable//search/replace} |
| SC2002 | Useless cat. Consider cmd < file or cmd file |
| SC2003 | expr is antiquated. Consider using $((..)) |
| SC2004 | $/${} unnecessary on arithmetic variables |
| SC2005 | Useless echo. Instead of echo $(cmd), use cmd |
| SC2006 | Use $(...) notation instead of legacy backticks |
| SC2007 | Use $((..)) instead of deprecated $[..] |
| SC2008 | echo doesn't read from stdin, use cat |
| SC2009 | Consider using pgrep instead of grepping ps |
| SC2010 | Don't use ls |
| SC2012 | Use find instead of ls to handle non-alphanumeric |
| SC2013 | To read lines, use while read or mapfile |
| SC2014 | This will expand once before find runs, not per file |
| SC2015 | Note that `A && B |
| SC2016 | Expressions don't expand in single quotes |
| SC2017 | Increase by assigning x=$((x+1)) |
| SC2018 | Use [:lower:] to match lowercase |
| SC2019 | Use [:upper:] to match uppercase |
| SC2020 | tr expects characters, not words |
| SC2021 | Don't use a-z or A-Z in tr brackets |
| SC2022 | Use newlines or semicolons between actions |
| SC2024 | sudo doesn't affect redirects |
| SC2025 | Don't use variables in printf format string |
| SC2026 | This word is outside any quotes |
| SC2027 | Quotes around $var prevent expansion |
| SC2028 | echo may not expand escape sequences |
| SC2029 | Note that ssh .. "$VAR" expands on client |
| SC2030 | Modification in subshell doesn't affect parent |
| SC2031 | var was modified in subshell, parent unaffected |
| SC2032 | Use own script or sh -c to nohup |
| SC2033 | Shell functions can't be passed to external commands |
| SC2034 | Variable appears unused (verify or export it) |
| SC2035 | Use ./*glob* or -- *glob* to not start with - |
| SC2036 | If you need absolute path, use $PWD/foo |
| SC2037 | Add space between function name and body |
| SC2038 | Use -print0/-0 or -d '\n' with xargs |
| SC2039 | In POSIX sh, this is undefined |
| SC2040 | #!/bin/sh wasn't specified but script uses bash |
| SC2041 | This is a literal string (use ranges or classes) |
| SC2043 | This loop will only run once with a constant |
| SC2044 | For loops over find output are fragile |
| SC2045 | Iterating over ls output is fragile |
| SC2046 | Quote this to prevent word splitting |
| SC2048 | Use "$@" (with quotes) to prevent whitespace issues |
| SC2049 | =~ is for regex, use == for wildcard |
| SC2050 | This expression is constant, use if true/false |
| SC2051 | Bash doesn't support variables in brace ranges |
| SC2053 | Quote RHS of != to prevent glob interpretation |
| SC2054 | Use spaces after ( and before ) in arrays |
| SC2055 | You probably wanted `&& here |
| SC2056 | You probably wanted ` |
| SC2057 | Unknown binary operator |
| SC2058 | Unknown unary operator |
| SC2059 | Don't use variables in printf format string |
| SC2060 | Quote to prevent word splitting and globbing |
| SC2061 | Quote the regex to prevent shell expansion |
| SC2062 | Quote the regex so it matches literally |
| SC2063 | Grep uses regex, not globs |
| SC2064 | Use single quotes for trap commands |
| SC2065 | This is interpreted as a shell file descriptor |
| SC2066 | This expression won't return failures |
| SC2067 | Missing ; or \; for end of -exec command |
| SC2068 | Double quote array expansions to prevent word splitting |
| SC2069 | To redirect stdout+stderr, 2>&1 must be last |
| SC2070 | -n doesn't work with unquoted arguments |
| SC2071 | > is for string comparisons, use -gt for numbers |
| SC2072 | Decimals not supported. Use bc or awk |
| SC2073 | Can't compare numbers with < |
| SC2074 | Can't use =~ in [, use [[ |
| SC2076 | Don't quote regex patterns in =~ |
| SC2077 | You need spaces around the comparison operator |
| SC2078 | This expression is constant, quote one argument |
| SC2079 | (( 2.7 )) may not work as expected |
| SC2080 | Numbers with leading zeros are octal |
| SC2081 | [ .. ] can't match globs, use a for loop |
| SC2082 | Use * for glob but .* for regex |
| SC2083 | Don't add spaces after the slash |
| SC2084 | Remove $ to add a number to a variable |
| SC2086 | Double quote to prevent globbing and word splitting |
| SC2087 | Quote all here-doc words or escape $ |
| SC2088 | Tilde does not expand in quotes |
| SC2089 | Quotes/escapes will be literal |
| SC2090 | Quotes/escapes in this variable will be literal |
| SC2091 | Remove surrounding $() to call command |
| SC2092 | Remove backticks surrounding assignment |
| SC2093 | Remove exec & if script should continue |
| SC2094 | Make sure not to read/write same file in pipeline |
| SC2095 | Command may try to read stdin |
| SC2096 | On most OS, shebangs can't have multiple args |
| SC2097 | This assignment is only in this command's env |
| SC2098 | This expansion won't happen |
| SC2099 | Use $((..)) for arithmetic |
| SC2100 | Use $((..)) for arithmetic |
Variable Issues
| Code | Description |
|---|---|
| SC2102 | Ranges can only match single characters |
| SC2103 | Consider using cd with ` |
| SC2104 | In functions, return instead of continue |
| SC2105 | break only in loops |
| SC2106 | SC2105 but for break |
| SC2107 | Instead of [ -n $foo -o -n $bar ], use `[ -n "$foo" ] |
| SC2108 | In [], use &&/` |
| SC2109 | Instead of -a/-o, use &&/` |
| SC2110 | In [[]], use &&/` |
| SC2111 | ksh does not support -a/-o |
| SC2112 | function already declares function |
| SC2114 | Avoid rm -rf paths with quoted paths |
| SC2115 | Avoid rm -rf paths with unquoted variables |
| SC2116 | Useless echo? Instead of cmd $(echo foo) |
| SC2117 | Use su -c cmd instead of su; cmd |
| SC2119 | Use foo "$@" to pass args through functions |
| SC2120 | References args but none passed |
| SC2121 | Create array with arr=(a b), not arr=a arr+=b |
| SC2122 | += is not possible in numbers |
| SC2123 | PATH is the system path. Use another variable |
| SC2124 | Assigning array to string. Use "${arr[*]}" |
| SC2125 | Brace expansion doesn't work in [ |
| SC2126 | Consider using grep -c instead of `grep |
| SC2128 | Expanding array without index gives first element |
| SC2129 | Consider using { cmd1; cmd2; } redirection |
| SC2130 | -eq is for numbers, not strings |
| SC2139 | This expands at definition, not execution |
| SC2140 | Words in braces need quoting for spaces |
| SC2141 | Did you mean IFS=$'\t' or IFS='$(printf '\t')' |
| SC2142 | Aliases can't take arguments. Use functions |
| SC2143 | Use grep -q instead of comparing output |
| SC2144 | -e doesn't work with globs. Use a for loop |
| SC2145 | Use "${arr[*]}" or separate "${arr[@]}" |
| SC2146 | This action ignores everything before -name |
| SC2147 | Literal tilde in PATH doesn't expand |
| SC2148 | Tips depend on shell. Add shebang |
| SC2149 | Make sure to quote interpolated variables |
| SC2150 | -exec .. {} + can't be followed by more flags |
| SC2151 | Only one integer 0-255 for exit |
| SC2152 | Can only return 0-255 from functions |
| SC2153 | Possible misspelling of DEFINED |
| SC2154 | var is referenced but not assigned |
| SC2155 | Declare and assign separately to avoid masking |
| SC2156 | Inject argument via ..- {} doesn't expand |
| SC2157 | Argument to implicit -n is a literal string |
| SC2158 | [ false ] is true. Use if ! or [[]] |
| SC2159 | [ 0 ] is true. Use (( )) for arithmetic |
| SC2160 | Use true command instead of [ 1 ] |
| SC2161 | Instead of [ expr ], use ((expr)) or [[ n -gt 0 ]] |
| SC2162 | read without -r mangles backslashes |
| SC2163 | This does not export the variable |
| SC2164 | Use `cd .. |
| SC2165 | Wrap this in a loop? Use break/exit correctly |
| SC2166 | Prefer [ p ] && [ q ] in sh |
| SC2167 | This doesn't assign the output of cmd |
| SC2168 | local is only valid in functions |
| SC2169 | In dash, this is undefined |
| SC2170 | Numerical -eq used for string |
| SC2171 | Expected test after elif |
| SC2172 | Trapping signals by number is not portable |
| SC2173 | SIGKILL can't be trapped |
| SC2174 | mkdir -p -m creates only final dir with perms |
| SC2175 | Quote to prevent word splitting |
| SC2176 | time is undefined for pipelines |
| SC2177 | time is undefined for compound |
| SC2178 | Variable was used as array but assigned string |
| SC2179 | Use array+=("item") to append to arrays |
| SC2180 | Bash brace expansion is not supported |
| SC2181 | Check exit code directly, not $? |
| SC2182 | This printf format doesn't match args |
| SC2183 | printf format string has more format specs than args |
| SC2184 | Quote args to unset to handle special chars |
| SC2185 | Some finds don't accept path after expression |
| SC2186 | tempfile is deprecated. Use mktemp |
| SC2187 | Ash scripts need -- before -e |
| SC2188 | This redirection has nothing |
| SC2189 | You can't have |
| SC2190 | Elements in associative array need index |
| SC2191 | Elements in indexed arrays need no index |
| SC2192 | This array element has no value |
| SC2193 | This comparison is constant |
| SC2194 | This word is constant |
| SC2195 | Pattern won't match with / |
| SC2196 | egrep is non-standard and deprecated |
| SC2197 | fgrep is non-standard and deprecated |
| SC2198 | Arrays don't work in [=]. Use [[==]] |
| SC2199 | Arrays expand separately |
| SC2200 | Glob used where integer expected |
| SC2201 | This glob only matches paths with / |
| SC2202 | Globs are lowercase. Use echo 'text' |
| SC2203 | Glob only matches if expanded |
| SC2204 | (...) is a subshell. Did you mean [...] |
SC3xxx - POSIX Compatibility
Warnings for non-POSIX features in sh scripts.
| Code | Description |
|---|---|
| SC3001 | In POSIX sh, $'..' is undefined |
| SC3002 | In POSIX sh, extglob is undefined |
| SC3003 | In POSIX sh, $'...' is undefined |
| SC3004 | In POSIX sh, $".." is undefined |
| SC3005 | In POSIX sh, {..} brace expansion is undefined |
| SC3006 | In POSIX sh, [[ is undefined |
| SC3007 | In POSIX sh, (( is undefined |
| SC3008 | In POSIX sh, select is undefined |
| SC3009 | In POSIX sh, &> is undefined |
| SC3010 | In POSIX sh, coproc is undefined |
| SC3011 | In POSIX sh, here-strings are undefined |
| SC3012 | In POSIX sh, lexicographic comparison is undefined |
| SC3013 | In POSIX sh, -v is undefined |
| SC3014 | In POSIX sh, =~ is undefined |
| SC3015 | In POSIX sh, >&X redirects are undefined |
| SC3016 | In POSIX sh, >&filename is undefined |
| SC3017 | In POSIX sh, <(cmd) is undefined |
| SC3018 | In POSIX sh, process substitution is undefined |
| SC3019 | In POSIX sh, readonly scoping is undefined |
| SC3020 | In POSIX sh, &> is undefined |
| SC3024 | In POSIX sh, -o pipefail is undefined |
| SC3028 | In POSIX sh, BASHPID is undefined |
| SC3030 | In POSIX sh, arrays are undefined |
| SC3033 | In POSIX sh, declare is undefined |
| SC3035 | In POSIX sh, read -r with arrays is undefined |
| SC3036 | In POSIX sh, echo -n is undefined |
| SC3037 | In POSIX sh, echo -e is undefined |
| SC3039 | In POSIX sh, let is undefined |
| SC3040 | In POSIX sh, set -o is undefined |
| SC3044 | In POSIX sh, local is undefined |
| SC3045 | In POSIX sh, read -t is undefined |
| SC3046 | In POSIX sh, source is undefined |
| SC3047 | In POSIX sh, this signal is undefined |
| SC3048 | In POSIX sh, printf %q is undefined |
| SC3050 | In POSIX sh, mapfile is undefined |
| SC3054 | In POSIX sh, array references are undefined |
| SC3055 | In POSIX sh, array key expansion is undefined |
| SC3056 | In POSIX sh, += is undefined |
| SC3057 | In POSIX sh, string indexing is undefined |
| SC3058 | In POSIX sh, ${var/pat/str} is undefined |
| SC3059 | In POSIX sh, indirect expansion is undefined |
| SC3060 | In POSIX sh, ${var:start} is undefined |
Severity Levels
ShellCheck categorizes warnings by severity:
| Level | Description |
|---|---|
| error | Definite bugs or syntax errors |
| warning | Likely issues that could cause problems |
| info | Suggestions for better practices |
| style | Purely stylistic suggestions |
Wiki Links
Each code has detailed documentation at: https://www.shellcheck.net/wiki/SCXXXX
For example: https://www.shellcheck.net/wiki/SC2086
ShellCheck CI/CD Integration Guide
Complete guide for integrating ShellCheck into development workflows.
Pre-commit Hooks
Official Pre-commit Hook
Add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
hooks:
- id: shellcheck
# Optional: customize arguments
args: ["--severity=warning"]Alternative: shellcheck-py
repos:
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.5
hooks:
- id: shellcheckCustom Pre-commit Script
#!/bin/bash
# .git/hooks/pre-commit
# Find all staged shell scripts
staged_scripts=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(sh|bash|zsh)$')
if [[ -n "$staged_scripts" ]]; then
echo "Running ShellCheck on staged scripts..."
# Run shellcheck on all staged scripts
if ! echo "$staged_scripts" | xargs shellcheck; then
echo "ShellCheck found issues. Please fix them before committing."
exit 1
fi
fi
exit 0Make executable:
chmod +x .git/hooks/pre-commitGitHub Actions
Basic Workflow
# .github/workflows/shellcheck.yml
name: ShellCheck
on:
push:
branches: [main, develop]
paths:
- '**.sh'
- '**.bash'
pull_request:
paths:
- '**.sh'
- '**.bash'
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
severity: warning
scandir: './scripts'
format: gcc
additional_files: 'entrypoint'Advanced Workflow with Annotations
name: ShellCheck
on: [push, pull_request]
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get install -y shellcheck
- name: Find shell scripts
id: scripts
run: |
scripts=$(find . -type f \( -name "*.sh" -o -name "*.bash" \) ! -path "./.git/*")
echo "scripts<<EOF" >> $GITHUB_OUTPUT
echo "$scripts" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Run ShellCheck
run: |
echo "${{ steps.scripts.outputs.scripts }}" | xargs shellcheck -f gcc || true
- name: ShellCheck with annotations
run: |
echo "${{ steps.scripts.outputs.scripts }}" | xargs shellcheck -f json | \
jq -r '.[] | "::warning file=\(.file),line=\(.line),col=\(.column)::\(.message) [\(.code)]"'Matrix Testing Multiple Shells
name: ShellCheck Multi-Shell
on: [push, pull_request]
jobs:
shellcheck:
runs-on: ubuntu-latest
strategy:
matrix:
shell: [sh, bash, dash, ksh]
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck for ${{ matrix.shell }}
run: |
find ./scripts -name "*.sh" -exec shellcheck -s ${{ matrix.shell }} {} +GitLab CI/CD
Basic Pipeline
# .gitlab-ci.yml
shellcheck:
image: koalaman/shellcheck-alpine:stable
stage: test
script:
- find . -name "*.sh" -exec shellcheck {} +
only:
changes:
- "**/*.sh"With Artifacts
shellcheck:
image: koalaman/shellcheck-alpine:stable
stage: lint
script:
- find . -name "*.sh" -print0 | xargs -0 shellcheck -f checkstyle > shellcheck-report.xml || true
artifacts:
reports:
junit: shellcheck-report.xml
paths:
- shellcheck-report.xml
expire_in: 1 weekJenkins
Declarative Pipeline
pipeline {
agent any
stages {
stage('ShellCheck') {
steps {
sh '''
find . -name "*.sh" | xargs shellcheck -f checkstyle > shellcheck.xml || true
'''
}
post {
always {
recordIssues tools: [checkStyle(pattern: 'shellcheck.xml')]
}
}
}
}
}With Docker
pipeline {
agent {
docker {
image 'koalaman/shellcheck-alpine:stable'
}
}
stages {
stage('Lint') {
steps {
sh 'find . -name "*.sh" -exec shellcheck {} +'
}
}
}
}CircleCI
# .circleci/config.yml
version: 2.1
orbs:
shellcheck: circleci/shellcheck@3.2.0
workflows:
lint:
jobs:
- shellcheck/check:
dir: ./scripts
severity: warning
exclude: SC1091Travis CI
# .travis.yml
language: shell
addons:
apt:
packages:
- shellcheck
script:
- find . -name "*.sh" -exec shellcheck {} +Azure DevOps
# azure-pipelines.yml
trigger:
paths:
include:
- '**/*.sh'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Bash@3
displayName: 'Install ShellCheck'
inputs:
targetType: 'inline'
script: |
sudo apt-get update
sudo apt-get install -y shellcheck
- task: Bash@3
displayName: 'Run ShellCheck'
inputs:
targetType: 'inline'
script: |
find . -name "*.sh" -exec shellcheck -f gcc {} +Makefile Integration
# Makefile
SHELL_SCRIPTS := $(shell find . -name "*.sh" -not -path "./.git/*")
.PHONY: lint lint-fix check-shellcheck
# Check if shellcheck is installed
check-shellcheck:
@command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck not installed. Run: brew install shellcheck"; exit 1; }
# Run shellcheck
lint: check-shellcheck
@echo "Running ShellCheck on $(words $(SHELL_SCRIPTS)) scripts..."
@shellcheck $(SHELL_SCRIPTS)
# Run with severity filter
lint-warnings: check-shellcheck
@shellcheck -S warning $(SHELL_SCRIPTS)
lint-errors: check-shellcheck
@shellcheck -S error $(SHELL_SCRIPTS)
# Generate diff for auto-fixes
lint-fix: check-shellcheck
@shellcheck -f diff $(SHELL_SCRIPTS) | patch -p1
# Generate JSON report
lint-report: check-shellcheck
@shellcheck -f json $(SHELL_SCRIPTS) > shellcheck-report.json
# CI mode - fail on any issue
ci-lint: check-shellcheck
@shellcheck -S warning -f gcc $(SHELL_SCRIPTS)Editor Integration
VS Code
Install vscode-shellcheck extension:
// .vscode/settings.json
{
"shellcheck.enable": true,
"shellcheck.run": "onSave",
"shellcheck.executablePath": "shellcheck",
"shellcheck.exclude": ["SC1091"],
"shellcheck.customArgs": ["-x"]
}Vim/Neovim with ALE
" ~/.vimrc or init.vim
let g:ale_linters = {
\ 'sh': ['shellcheck'],
\ 'bash': ['shellcheck'],
\}
let g:ale_sh_shellcheck_options = '-x'
let g:ale_sh_shellcheck_exclusions = 'SC1091'Neovim with nvim-lint
-- lua/plugins/lint.lua
require('lint').linters_by_ft = {
sh = {'shellcheck'},
bash = {'shellcheck'},
}
require('lint').linters.shellcheck.args = {
'-x',
'-f', 'gcc',
'-',
}Emacs with Flycheck
;; ~/.emacs.d/init.el
(use-package flycheck
:ensure t
:init (global-flycheck-mode)
:config
(setq flycheck-shellcheck-follow-sources nil)
(add-to-list 'flycheck-disabled-checkers 'sh-posix-dash))Sublime Text
Install SublimeLinter-shellcheck via Package Control.
Docker Usage
Basic Docker Run
# Check single file
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable /mnt/script.sh
# Check directory
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable /mnt/**/*.sh
# With options
docker run --rm -v "$PWD:/mnt" \
-e SHELLCHECK_OPTS='-x -S warning' \
koalaman/shellcheck:stable /mnt/scripts/*.shDocker Compose
# docker-compose.yml
version: '3.8'
services:
shellcheck:
image: koalaman/shellcheck:stable
volumes:
- ./:/mnt:ro
- ./.shellcheckrc:/root/.shellcheckrc:ro
command: /mnt/scripts/*.shMulti-stage Dockerfile with ShellCheck
# Dockerfile
FROM koalaman/shellcheck:stable AS shellcheck
COPY scripts/ /scripts/
RUN shellcheck /scripts/*.sh
FROM ubuntu:22.04
COPY --from=shellcheck /scripts/ /app/scripts/
# ... rest of buildQuality Gate Integration
SonarQube
Use CheckStyle format and import:
shellcheck -f checkstyle *.sh > shellcheck-checkstyle.xmlCodacy
ShellCheck is natively supported. Enable in repository settings.
Code Climate
Add to .codeclimate.yml:
version: "2"
plugins:
shellcheck:
enabled: true
config:
severity: warningCodeFactor
ShellCheck is enabled by default for shell scripts.
Analyze Workflow
Analyze shell scripts with ShellCheck and present findings.
Trigger
- "shellcheck this script"
- "lint my shell script"
- "check for shell errors"
- "analyze bash script"
Process
1. Identify Scripts
Find shell scripts to analyze:
# Single file provided
shellcheck "$FILE"
# Find all scripts in directory
find . -type f \( -name "*.sh" -o -name "*.bash" -o -name "*.zsh" \) ! -path "./.git/*"
# Or detect by shebang
find . -type f ! -path "./.git/*" -exec sh -c '
head -1 "$1" | grep -qE "^#!.*(bash|sh|zsh|ksh)" && echo "$1"
' _ {} \;2. Run ShellCheck
# JSON output for parsing
shellcheck -f json "$SCRIPTS" 2>/dev/null
# GCC format for quick viewing
shellcheck -f gcc "$SCRIPTS"
# With all optional checks enabled
shellcheck -o all -f json "$SCRIPTS"3. Parse and Present Results
Group findings by severity:
# Count by severity
shellcheck -f json script.sh | jq 'group_by(.level) | map({level: .[0].level, count: length})'4. Output Format
Present findings in structured format:
## ShellCheck Analysis: script.sh
### Summary
- **Errors:** 2
- **Warnings:** 5
- **Info:** 3
- **Style:** 1
### Findings
#### Errors (Must Fix)
**SC2086** (line 15): Double quote to prevent globbing and word splitting.Line 15
echo $variable
Fix
echo "$variable"
[Wiki: SC2086](https://www.shellcheck.net/wiki/SC2086)
#### Warnings
**SC2046** (line 23): Quote this to prevent word splitting.Line 23
files=$(ls *.txt)
Fix
files="$(ls *.txt)"
[Wiki: SC2046](https://www.shellcheck.net/wiki/SC2046)
### Quick Fixes
Apply auto-fixes with:shellcheck -f diff script.sh | patch -p1
Example Invocations
# Basic analysis
shellcheck script.sh
# Strict analysis (all checks)
shellcheck -o all -S warning script.sh
# Check multiple files
shellcheck scripts/*.sh
# Recursive directory
find . -name "*.sh" -exec shellcheck {} +
# With specific shell
shellcheck -s bash script.shIntegration with Workflows
After analysis, suggest:
1. Auto-fix available issues:
shellcheck -f diff script.sh | patch -p12. Configure suppressions for false positives
3. Set up pre-commit hook for continuous validation
Explain Workflow
Explain ShellCheck error codes and provide context-specific guidance.
Trigger
- "explain SC2086"
- "what does SC2086 mean"
- "shellcheck error SC2046"
- "help with SC code"
Process
1. Parse Error Code
Extract the SC code from user input:
SC20862086shellcheck SC2086
2. Fetch Documentation
Reference the wiki URL:
https://www.shellcheck.net/wiki/SCXXXX3. Provide Explanation
Structure the response:
1. What it means - Plain language explanation 2. Why it matters - The actual problem it catches 3. How to fix - Code examples 4. When to suppress - Legitimate exceptions
Common Error Explanations
SC2086 - Double quote to prevent globbing and word splitting
What it means: Variable is unquoted, which allows word splitting and glob expansion.
Why it matters:
# If filename contains spaces or glob characters
filename="my file*.txt"
# Bad - expands to multiple arguments or matches files
rm $filename
# Could become:
rm my file*.txt # 3 separate args!
# Or expand globs to match actual filesHow to fix:
rm "$filename"When to suppress:
- Intentionally splitting a variable:
# shellcheck disable=SC2086 - Variable contains intentional glob pattern
---
SC2046 - Quote this to prevent word splitting
What it means: Command substitution output is unquoted.
Why it matters:
# Bad - output split on whitespace
files=$(find . -name "*.txt")
for f in $files; do # Breaks on spaces in filenames
process "$f"
doneHow to fix:
# Use arrays
mapfile -t files < <(find . -name "*.txt")
for f in "${files[@]}"; do
process "$f"
done
# Or use while read
find . -name "*.txt" -print0 | while IFS= read -r -d '' f; do
process "$f"
done---
SC2034 - Variable appears unused
What it means: A variable is assigned but never used in the script.
Why it matters:
- Dead code that should be removed
- Possible typo in variable name
- Variable meant for external use
How to fix:
# If truly unused, remove it
# readonly UNUSED_VAR="value" # Delete
# If used externally, export it
export CONFIG_PATH="/etc/myapp"
# If false positive, disable with explanation
# shellcheck disable=SC2034 # Used by sourced scripts
readonly LIB_VERSION="1.0.0"---
SC2154 - Variable is referenced but not assigned
What it means: Using a variable that was never set.
Why it matters:
- Typo in variable name
- Missing assignment
- Variable expected from environment/sourced file
How to fix:
# Ensure assignment
MY_VAR="value"
echo "$MY_VAR"
# For environment variables, check existence
if [[ -z ${ENV_VAR:-} ]]; then
echo "ENV_VAR not set" >&2
exit 1
fi
# For sourced files, add directive
# shellcheck source=./config.sh
source "$CONFIG_FILE"---
SC2155 - Declare and assign separately
What it means: local declaration combined with command substitution masks the exit code.
Why it matters:
# Bad - exit code of 'command' is lost
local output=$(command_that_might_fail)
# $? is always 0 (from successful 'local')
# If command_that_might_fail returns 1, you won't knowHow to fix:
# Declare separately
local output
output=$(command_that_might_fail) || return 1---
SC2164 - Use 'cd ... || exit' in case cd fails
What it means: cd can fail, and subsequent commands would run in wrong directory.
Why it matters:
# Dangerous!
cd /some/directory
rm -rf * # If cd failed, deletes from current directory!How to fix:
cd /some/directory || exit 1
rm -rf ./*
# Or with error handling
cd /some/directory || {
echo "Failed to cd to /some/directory" >&2
exit 1
}---
SC1090 - Can't follow non-constant source
What it means: ShellCheck can't analyze a dynamically sourced file.
Why it matters: ShellCheck needs to know what's being sourced to check for issues.
How to fix:
# Tell ShellCheck where to find the file
# shellcheck source=./lib/common.sh
source "$LIB_DIR/common.sh"
# Or skip if file doesn't exist at check time
# shellcheck source=/dev/null
source "$DYNAMIC_CONFIG"---
SC2006 - Use $(...) instead of backticks
What it means: Using legacy backtick syntax for command substitution.
Why it matters:
- Backticks are harder to read
- Can't be nested easily
- Quoting rules are confusing
How to fix:
# Bad
date=`date +%Y-%m-%d`
nested=`echo \`hostname\``
# Good
date=$(date +%Y-%m-%d)
nested=$(echo "$(hostname)")---
SC2162 - read without -r will mangle backslashes
What it means: read interprets backslashes as escape sequences without -r.
Why it matters:
# Bad
echo "path\to\file" | read line
echo "$line" # Outputs: pathoile (backslashes interpreted)How to fix:
# Use -r for raw input
while IFS= read -r line; do
echo "$line"
done < fileOutput Template
## SC{CODE}: {Title}
### Explanation
{Plain language explanation}
### Why This Matters
{Real-world consequences}
### Before (Problematic){bad code}
### After (Fixed){fixed code}
### When to Suppress
{Legitimate exceptions with example}
shellcheck disable=SC{CODE}
Reason: {justification}
{code}
### Related Codes
- SC{related1}: {description}
- SC{related2}: {description}
### Resources
- [Wiki: SC{CODE}](https://www.shellcheck.net/wiki/SC{CODE})Fix Workflow
Apply ShellCheck fixes to shell scripts.
Trigger
- "fix shellcheck errors"
- "apply shellcheck fixes"
- "auto-fix shell script"
- "fix SC2086"
Process
1. Generate Diff
# Generate unified diff for all fixable issues
shellcheck -f diff script.sh > fixes.patch
# Preview changes
cat fixes.patch2. Review Fixes
Present the diff to user for review:
# Show with color
diff -u original.sh fixed.sh | colordiff
# Or use git diff
git diff --no-index original.sh fixed.sh3. Apply Fixes
Option A: Apply patch
shellcheck -f diff script.sh | patch -p1Option B: Apply with git
shellcheck -f diff script.sh | git applyOption C: Selective application
# Apply only specific files
shellcheck -f diff script.sh | patch -p1 --dry-run # Preview
shellcheck -f diff script.sh | patch -p1 # Apply4. Verify Fixes
# Re-run shellcheck to confirm
shellcheck script.sh
# Should show fewer/no issues
echo "Exit code: $?" # 0 = cleanCommon Fixes
SC2086 - Quote Variables
Before:
echo $variable
rm $filesAfter:
echo "$variable"
rm "$files"SC2046 - Quote Command Substitution
Before:
files=$(find . -name "*.txt")After:
files="$(find . -name "*.txt")"SC2006 - Use $() Instead of Backticks
Before:
date=`date +%Y-%m-%d`After:
date=$(date +%Y-%m-%d)SC2155 - Declare and Assign Separately
Before:
local output=$(command)After:
local output
output=$(command)SC2164 - Add || exit After cd
Before:
cd /some/directory
rm -rf *After:
cd /some/directory || exit 1
rm -rf ./*Batch Fix Multiple Files
#!/bin/bash
# fix-all-scripts.sh
set -euo pipefail
# Find all shell scripts
scripts=$(find . -name "*.sh" -o -name "*.bash" | grep -v ".git")
for script in $scripts; do
echo "Fixing: $script"
# Generate and apply fixes
if diff=$(shellcheck -f diff "$script" 2>/dev/null); then
if [[ -n "$diff" ]]; then
echo "$diff" | patch -p1
echo " Applied fixes"
else
echo " No fixes needed"
fi
else
echo " Error running shellcheck"
fi
done
echo "Done. Re-running shellcheck..."
find . -name "*.sh" -exec shellcheck {} + || trueManual Fix Patterns
When auto-fix isn't available:
Array Expansion
# Bad: SC2068
process ${array[@]}
# Good
process "${array[@]}"Read Without -r
# Bad: SC2162
while read line; do
# Good
while IFS= read -r line; doTest Command
# Bad: SC2070
[ -n $var ]
# Good
[[ -n $var ]]Output Format
After applying fixes, report:
## Fixes Applied
**Script:** deploy.sh
| Line | Code | Fix Applied |
|------|------|-------------|
| 15 | SC2086 | Quoted `$variable` |
| 23 | SC2046 | Quoted command substitution |
| 45 | SC2006 | Replaced backticks with `$()` |
**Remaining Issues:**
- SC2034 (line 8): Variable appears unused - manual review needed
- SC1090 (line 3): Can't follow dynamic source - add directive
**Commands:**Verify fixes
shellcheck deploy.sh
Add directive for SC1090
shellcheck source=/dev/null
source "$CONFIG_FILE"
Setup Workflow
Configure ShellCheck for a project with configuration file, pre-commit hooks, and CI integration.
Trigger
- "setup shellcheck for this project"
- "configure shellcheck"
- "add shellcheck to CI"
- "install shellcheck"
Process
1. Check Installation
# Check if installed
if command -v shellcheck >/dev/null 2>&1; then
echo "ShellCheck $(shellcheck --version | head -2 | tail -1) installed"
else
echo "ShellCheck not installed"
fi2. Install ShellCheck
macOS:
brew install shellcheckDebian/Ubuntu:
sudo apt-get update && sudo apt-get install -y shellcheckArch Linux:
sudo pacman -S shellcheckFedora:
sudo dnf install ShellCheckFrom Binary (Linux):
VERSION="v0.11.0"
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${VERSION}/shellcheck-${VERSION}.linux.x86_64.tar.xz" | tar -xJf -
sudo cp "shellcheck-${VERSION}/shellcheck" /usr/local/bin/3. Create .shellcheckrc
Create project configuration:
cat > .shellcheckrc << 'EOF'
# ShellCheck Configuration
# https://www.shellcheck.net/wiki/
# Shell dialect (sh, bash, dash, ksh, busybox)
shell=bash
# Source file handling
source-path=SCRIPTDIR
source-path=SCRIPTDIR/lib
external-sources=true
# Enable dataflow analysis
extended-analysis=true
# Enable optional checks
enable=deprecate-which
enable=quote-safe-variables
enable=require-variable-braces
# Disable common false positives (customize as needed)
# disable=SC1090 # Can't follow non-constant source
# disable=SC1091 # Not following sourced file
# disable=SC2034 # Variable appears unused
EOF4. Setup Pre-commit Hook
Option A: Using pre-commit framework
Create/update .pre-commit-config.yaml:
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
hooks:
- id: shellcheck
args: ["--severity=warning"]Install:
pip install pre-commit
pre-commit installOption B: Git hook directly
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
# ShellCheck pre-commit hook
set -euo pipefail
# Find staged shell scripts
staged=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(sh|bash)$' || true)
if [[ -n "$staged" ]]; then
echo "Running ShellCheck..."
if ! echo "$staged" | xargs shellcheck; then
echo ""
echo "ShellCheck found issues. Fix them or use:"
echo " git commit --no-verify"
exit 1
fi
echo "ShellCheck passed!"
fi
EOF
chmod +x .git/hooks/pre-commit5. Setup CI Pipeline
GitHub Actions:
Create .github/workflows/shellcheck.yml:
name: ShellCheck
on:
push:
branches: [main, develop]
paths:
- '**.sh'
- '**.bash'
- '.shellcheckrc'
pull_request:
paths:
- '**.sh'
- '**.bash'
- '.shellcheckrc'
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
severity: warning
scandir: '.'
format: gccGitLab CI:
Add to .gitlab-ci.yml:
shellcheck:
image: koalaman/shellcheck-alpine:stable
stage: lint
script:
- find . -name "*.sh" -not -path "./.git/*" -exec shellcheck {} +
only:
changes:
- "**/*.sh"
- ".shellcheckrc"6. Add Makefile Target
# Add to Makefile
SHELL_SCRIPTS := $(shell find . -name "*.sh" -not -path "./.git/*")
.PHONY: lint lint-shell
lint-shell:
@echo "Running ShellCheck..."
@shellcheck $(SHELL_SCRIPTS)
lint: lint-shell
@echo "All linters passed!"7. Editor Configuration
VS Code:
Create .vscode/settings.json:
{
"shellcheck.enable": true,
"shellcheck.run": "onSave",
"shellcheck.exclude": [],
"shellcheck.customArgs": ["-x"]
}Vim (with ALE):
Add to .vimrc or project .vimrc:
let g:ale_linters = {'sh': ['shellcheck'], 'bash': ['shellcheck']}
let g:ale_sh_shellcheck_options = '-x'Output Format
After setup, present summary:
## ShellCheck Setup Complete
### Files Created/Modified
| File | Purpose |
|------|---------|
| `.shellcheckrc` | Project configuration |
| `.pre-commit-config.yaml` | Pre-commit hook |
| `.github/workflows/shellcheck.yml` | CI pipeline |
| `Makefile` | Added `lint-shell` target |
### Quick Commands
Check all scripts
make lint-shell
Check single file
shellcheck script.sh
Auto-fix issues
shellcheck -f diff script.sh | patch -p1
Run pre-commit manually
pre-commit run shellcheck --all-files
### Configuration Summary
- **Shell:** bash
- **Optional checks:** deprecate-which, quote-safe-variables, require-variable-braces
- **Severity:** warning (CI), all (local)
- **Source paths:** SCRIPTDIR, SCRIPTDIR/lib
### Next Steps
1. Run `shellcheck scripts/*.sh` to check existing scripts
2. Fix any issues or add justified suppressions
3. Commit the configuration filesCustomization Options
Ask user about preferences:
1. Shell dialect: bash (default), sh, zsh, ksh 2. Strictness level: relaxed, standard (default), strict 3. Optional checks: which ones to enable 4. CI platform: GitHub Actions, GitLab CI, none 5. Pre-commit: pre-commit framework, git hook, none