skip to content

shellcheck — Shell Script Linter

Catch quoting bugs, missing checks, and POSIX portability mistakes in shell scripts. Covers every flag, severity levels, inline directives, CI/pre-commit integration, and the most common rules.

18 min read 72 snippets deep dive

shellcheck — Shell Script Linter#

What it is#

shellcheck is a static analysis tool for shell scripts, written in Haskell by Vidar Holen and the open-source community since 2012. It parses sh, bash, dash, and ksh scripts and emits warnings for quoting bugs, undefined variables, ignored exit codes, POSIX violations, and a long list of other footguns that would otherwise blow up at runtime. Reach for shellcheck on every shell script you write — wire it into your editor, pre-commit hook, and CI. There’s no real alternative; it’s effectively the standard linter for the language.

Install#

ShellCheck is packaged everywhere — distro repos, Homebrew, snap, scoop, even a Docker image. The standalone binary is also a single statically linked file you can drop onto any Linux host.

# Debian/Ubuntu
sudo apt install shellcheck

# RHEL/Fedora
sudo dnf install ShellCheck

# macOS
brew install shellcheck

# Arch
sudo pacman -S shellcheck

# Standalone binary
wget -qO- https://github.com/koalaman/shellcheck/releases/latest/download/shellcheck-stable.linux.x86_64.tar.xz \
  | tar -xJ --strip-components=1 -C /usr/local/bin shellcheck-stable/shellcheck

# Docker
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript.sh

# Verify
shellcheck --version

Output:

ShellCheck - shell script analysis tool
version: 0.11.0
license: GNU General Public License, version 3
website: https://www.shellcheck.net

v0.11.0 (Aug 2025) added several SC codes (SC2327–SC2332, SC3062), a new optional avoid-negated-conditions check (SC2335), and disabled SC2002 (“useless use of cat”) by default. v0.10.0 (Mar 2024) introduced --rcfile, the extended-analysis directive, and BusyBox sh support.

Syntax#

Pass one or more script paths; ShellCheck infers the dialect from the shebang or the file extension. Exit code is non-zero if any issue is found.

shellcheck [OPTIONS] FILE [FILE...]
shellcheck -                       # read script from stdin
find . -name '*.sh' -exec shellcheck {} +

Output: (none — exits 0 on success)

Essential options#

FlagMeaning
-s SHELLForce dialect: sh, bash, dash, ksh, bats
-S LEVELMinimum severity to report: error, warning, info, style
-e CODESExclude specific rules (-e SC2086,SC2155)
-i CODESOnly include these rules (whitelist)
-o CHECKSEnable optional checks (-o all recommended)
-f FORMATOutput format: tty (default), gcc, checkstyle, json, json1, diff, quiet
-xFollow source / . directives across files
-aCheck all files in sourced trees
-P PATHSource-path: additional dirs to search for source targets
-W NMax wiki-link width (cosmetic)
-VPrint version and exit
--severity=LEVELLong form of -S
--enable=CHECKSLong form of -o
--list-optionalShow available optional checks
--rcfile=PATHUse a specific .shellcheckrc instead of auto-discovery (v0.10.0+)
--norcSkip .shellcheckrc discovery entirely (v0.10.0+)
--extended-analysis=BOOLToggle dataflow-based checks like SC2317; equivalent to the extended-analysis directive

A first run#

The default output uses ANSI colours, highlights the offending line, points at the problem column, and lists the SCxxxx rule code with a one-line explanation and a wiki URL.

cat > greet.sh <<'EOF'
#!/usr/bin/env bash
name=$1
echo Hello $name, the time is `date`
EOF
shellcheck greet.sh

Output:

In greet.sh line 2:
name=$1
     ^-- SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
name="$1"

In greet.sh line 3:
echo Hello $name, the time is `date`
           ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.
                              ^---^ SC2006 (style): Use $(...) notation instead of legacy backticks `...`.

For more information:
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbi...
  https://www.shellcheck.net/wiki/SC2006 -- Use $(...) notation instead o...

Severity levels#

Every finding is one of four severities. Use -S to raise the floor — -S warning mutes info and style messages, which is helpful when triaging a legacy script.

LevelWhat it means
errorThe script is almost certainly broken (syntax, undefined refs)
warningThe script will likely misbehave in some inputs (quoting, globbing)
infoIdiomatic improvement; rarely outright wrong
styleCosmetic — backticks vs $(...), [ ] vs [[ ]], etc.
shellcheck -S error    deploy.sh        # only show real bugs
shellcheck -S warning  deploy.sh        # include quoting issues
shellcheck -S style    deploy.sh        # the maximalist run (default)

Output:

# -S error on a clean script:
(no output)

Inline directives#

Drop a # shellcheck comment to silence, configure, or scope a check. Directives apply to the next statement, the rest of the file, or the function they live in, depending on placement.

#!/usr/bin/env bash
# shellcheck shell=bash               # force the dialect for ambiguous files

# Disable one rule for the whole file (must be near the top)
# shellcheck disable=SC2086

# Disable a rule for just the next statement
# shellcheck disable=SC2046
files=$(find . -type f)

# Disable multiple rules
# shellcheck disable=SC2086,SC2155
export PATH=/usr/local/bin:$PATH

# Enable an optional check globally
# shellcheck enable=require-variable-braces

# Tell shellcheck about a sourced file it can't follow
# shellcheck source=./lib/util.sh
source "$LIB/util.sh"

Output: (none — exits 0 on success)

Use disables sparingly — if you’re disabling a rule on every script, it’s a sign the rule is correct and your codebase has a real bug class to fix.

Optional (off-by-default) checks#

shellcheck -o all turns on every optional check. The list is small but valuable: add-default-case, avoid-nullary-conditions, avoid-negated-conditions (v0.11.0+), quote-safe-variables, require-variable-braces, require-double-brackets, useless-use-of-cat (off by default since v0.11.0), and a few more. Run --list-optional to see them all.

shellcheck --list-optional
shellcheck -o all deploy.sh
shellcheck -o require-double-brackets,quote-safe-variables deploy.sh

Output (--list-optional):

name:    add-default-case
default: disabled
help:    Suggest adding a default case in `case` statements

name:    avoid-negated-conditions
default: disabled
help:    Suggest replacing `[ ! a -eq b ]` with `[ a -ne b ]` (SC2335)

name:    avoid-nullary-conditions
default: disabled
help:    Suggest explicitly using -n in `[ $var ]`

name:    check-extra-masked-returns
default: disabled
help:    Check for additional cases where exit codes are masked

name:    check-set-e-suppressed
default: disabled
help:    Notify when set -e is suppressed during function invocation

name:    check-unassigned-uppercase
default: disabled
help:    Warn when uppercase variables are unassigned

name:    deprecate-which
default: disabled
help:    Suggest 'command -v' instead of 'which'

name:    quote-safe-variables
default: disabled
help:    Suggest quoting variables without metacharacters

name:    require-double-brackets
default: disabled
help:    Require [[ and warn about [ ]

name:    require-variable-braces
default: disabled
help:    Require {} on every variable reference

name:    useless-use-of-cat
default: disabled
help:    Suggest piping/redirecting input instead of using cat (was SC2002 default until v0.11.0)

Project-wide configuration#

A .shellcheckrc sets defaults for every invocation. ShellCheck walks upward from each script’s directory and merges every .shellcheckrc it finds, so a repo-root file applies to every script under it and a lib/.shellcheckrc can override settings just for that subtree. As a last resort it also checks $XDG_CONFIG_HOME/shellcheckrc and $HOME/.shellcheckrc. Use this for team-wide rule choices instead of plastering disables across files.

# .shellcheckrc
shell=bash
severity=warning
enable=quote-safe-variables
enable=require-variable-braces
enable=avoid-negated-conditions    # v0.11.0+ (SC2335)
disable=SC2034                     # variable assigned but not used (we use indirection)
source-path=SCRIPTDIR
source-path=SCRIPTDIR/lib
external-sources=true
extended-analysis=true             # dataflow analysis (SC2317 unreachable, etc.)

Output: (none — exits 0 on success)

# Override discovery: point at a specific config
shellcheck --rcfile=./ci/shellcheckrc greet.sh

# Or bypass any discovered .shellcheckrc entirely
shellcheck --norc greet.sh

# Auto-discovery (default): walks parent dirs, then $XDG_CONFIG_HOME, then $HOME
shellcheck greet.sh

Output: (none — exits 0 on success)

Available directives#

Every directive below can live in a .shellcheckrc (one per line, key=value) or as an inline # shellcheck key=value comment.

DirectivePurpose
shell=sh|bash|dash|ksh|batsForce dialect when there’s no shebang
severity=error|warning|info|styleMinimum severity to report
enable=CHECK[,CHECK…]Turn on an optional check (or keyphrase like all)
disable=SC####[,SC####…]Suppress one or more rules; supports SC2000-SC2099 ranges
external-sources=true|falsePermit -x follow-through to sourced files
source=PATHTell ShellCheck where a non-constant source resolves
source-path=DIRSearch path for sourced files (SCRIPTDIR = script’s own dir)
extended-analysis=true|falseToggle dataflow-based checks (SC2317 unreachable, return-value tracing)

Following source and .#

By default ShellCheck warns when it can’t analyse a sourced file (SC1091). -x (or external-sources=true in .shellcheckrc) opens those files and analyses them in context, catching cross-file mistakes like a function used here but defined nowhere.

shellcheck -x deploy.sh

# Tell it where to look for sourced files
shellcheck -P ./lib:./common -x deploy.sh

# Or per-source hint
cat deploy.sh
# #!/usr/bin/env bash
# # shellcheck source=lib/util.sh
# source "$LIB_DIR/util.sh"

Output:

In deploy.sh line 4:
source "$LIB_DIR/util.sh"
       ^---------------^ SC1091 (info): Not following: $LIB_DIR/util.sh: openBinaryFile: does not exist (No such file or directory)

Output formats for tooling#

-f switches to a machine-readable format so editors, CI, and review bots can attach findings to lines.

shellcheck -f gcc        deploy.sh    # editor-friendly: file:line:col:level: msg
shellcheck -f checkstyle deploy.sh    # Jenkins / static-analysis dashboards
shellcheck -f json       deploy.sh    # array of objects
shellcheck -f json1      deploy.sh    # one object with "comments": [...]
shellcheck -f diff       deploy.sh    # unified diff with suggested fixes
shellcheck -f quiet      deploy.sh    # no output, just exit code

Output (-f gcc):

deploy.sh:5:8: note: Double quote to prevent globbing and word splitting. [SC2086]
deploy.sh:9:1: warning: Tabs and spaces in indentation. [SC1107]

Output (-f json1 — abridged):

{
  "comments": [
    {
      "file": "deploy.sh",
      "line": 5,
      "endLine": 5,
      "column": 8,
      "endColumn": 12,
      "level": "info",
      "code": 2086,
      "message": "Double quote to prevent globbing and word splitting.",
      "fix": {
        "replacements": [
          {"line":5,"column":8,"endLine":5,"endColumn":12,
           "precedence":7,"insertionPoint":"afterEnd","replacement":"\"$NAME\""}
        ]
      }
    }
  ]
}

Output (-f diff):

--- a/deploy.sh
+++ b/deploy.sh
@@ -2,3 +2,3 @@
-name=$1
-echo Hello $name, the time is `date`
+name="$1"
+echo Hello "$name", the time is "$(date)"

Applying suggested fixes automatically#

The diff format is a real unified diff you can pipe straight into patch. For larger codebases this is the fastest way to apply the safe quoting fixes.

shellcheck -f diff deploy.sh | patch -p1
git diff                    # review what changed
git restore deploy.sh       # if you don't like it

Output:

patching file deploy.sh

Most-hit rules — the top ten#

A short tour of the rules you’ll see daily. The number in parentheses is roughly how often each shows up in real codebases.

SC2086 — Double quote to prevent globbing and word splitting#

Variables and command substitutions undergo word splitting and pathname expansion unless quoted. This is the most common bug class in shell scripts.

# Bad
for f in $files; do rm $f; done

# Good
for f in "${files[@]}"; do rm -- "$f"; done

Output:

SC2086 (info): Double quote to prevent globbing and word splitting.

SC2155 — Declare and assign separately#

local, export, readonly, and declare set an exit code of their own, masking the exit code of the command on the right side.

# Bad — failure of `date` is hidden
local now=$(date -u +%FT%TZ)

# Good
local now
now=$(date -u +%FT%TZ) || return

Output:

SC2155 (warning): Declare and assign separately to avoid masking return values.

SC2046 — Quote to prevent word splitting in command substitution#

$(...) and `...` expand and then word-split unless quoted.

# Bad
rm $(find . -name '*.log')

# Good
find . -name '*.log' -delete
# or, when you really need a list:
find . -name '*.log' -print0 | xargs -0 rm --

Output:

SC2046 (warning): Quote this to prevent word splitting.

SC2181 — Check exit code directly, not via $?#

if [ $? -ne 0 ] is verbose, fragile, and wrong if any other command runs between.

# Bad
make build
if [ $? -ne 0 ]; then echo "failed"; exit 1; fi

# Good
if ! make build; then echo "failed"; exit 1; fi

Output:

SC2181 (style): Check exit code directly with e.g. `if mycmd;`, not indirectly with $?.

SC2164 — cd may fail; check it#

A cd that fails leaves you in the wrong directory; the rest of the script then runs against the original cwd.

# Bad
cd /var/log
rm *.gz                # wrong if cd failed

# Good
cd /var/log || exit
rm /var/log/*.gz       # or be explicit about the path

Output:

SC2164 (warning): Use 'cd ... || exit' or similar to handle cd failures.

SC2068 — Double quote array expansions#

$@ and $* (and similarly ${arr[@]}) must be quoted to preserve element boundaries.

# Bad
mycmd $@

# Good
mycmd "$@"

Output:

SC2068 (error): Double quote array expansions to avoid re-splitting elements.

SC2034 — Variable appears unused#

ShellCheck didn’t see the variable read anywhere. False positives are common when you use indirect references (${!name}); silence with # shellcheck disable=SC2034 or add an underscore prefix.

# False-positive case (genuine indirection)
ENV_PROD=prod
ENV_STAGE=stage
key=PROD
# shellcheck disable=SC2034
echo "${!key}"

Output:

SC2034 (warning): ENV_PROD appears unused. Verify use (or export if used externally).

SC1090 / SC1091 — Can’t follow non-constant source#

source $LIB is opaque to ShellCheck unless you hint where to look.

# shellcheck source=lib/common.sh
source "$LIB_DIR/common.sh"

Output:

SC1090 (warning): ShellCheck can't follow non-constant source. Use a directive to specify location.

SC2006 — Use $(...) instead of backticks#

Backticks nest poorly and look like apostrophes.

# Bad
ver=`git describe --tags`

# Good
ver=$(git describe --tags)

Output:

SC2006 (style): Use $(...) notation instead of legacy backticks `...`.

SC2207 — Avoid array=( $(cmd) )#

That pattern word-splits, glob-expands, and loses any element with a space.

# Bad
files=( $(ls *.txt) )

# Good (bash 4+)
mapfile -t files < <(ls -1 *.txt)

# Or POSIX-ish
readarray -t files < <(find . -maxdepth 1 -name '*.txt')

Output:

SC2207 (warning): Prefer mapfile or read -a to split command output (or quote to avoid splitting).

Newer rules (v0.10.0 and v0.11.0)#

ShellCheck keeps adding rules; here are the ones you’re most likely to hit on a recently-upgraded toolchain. The big behavioural change in v0.11.0 is that SC2002 (“useless use of cat”) was demoted to an opt-in optional check — long-standing complaints that it caused more noise than insight finally won.

SC2324 — x+=1 appends to a string, not increments#

In Bash, += on an unset or string variable concatenates. To increment a number use (( x++ )) or (( x += 1 )).

# Bad — produces the string "01", not the number 1
x=0
x+=1

# Good
x=0
(( x += 1 ))

Output:

SC2324 (warning): x+=1 will append, not increment. Use (( x += 1 )) or x=$((x + 1)).

SC2327 / SC2328 — Don’t capture the output of a redirected command#

var=$(cmd > file) redirects stdout to the file and captures the (now-empty) stream into var. Almost always a bug.

# Bad
log=$(make build > build.log)

# Good — pick one
log=$(make build); printf '%s\n' "$log" > build.log
make build > build.log

Output:

SC2327 (warning): This command redirects its standard output, so the assignment will be empty.

SC2329 — Function is never invoked#

ShellCheck now flags functions that are defined but never called anywhere in the script (or its -x-followed sources). Indirect callers ("$func", traps, complete -F) need a # shellcheck disable=SC2329.

# Bad
cleanup() { rm -rf "$tmp"; }
# (no caller anywhere)

# Good
cleanup() { rm -rf "$tmp"; }
trap cleanup EXIT

Output:

SC2329 (info): This function is never invoked. Verify name, or make it a static analysis comment.

SC2331 — Use -e, not unary -a#

[ -a file ] is a non-portable alias for [ -e file ] and collides with the binary AND operator.

# Bad
[ -a "$path" ] && echo "exists"

# Good
[ -e "$path" ] && echo "exists"

Output:

SC2331 (warning): Use -e instead of unary -a (which is bashism and not POSIX).

SC2002 — Useless use of cat (now opt-in)#

Demoted to optional in v0.11.0. Re-enable with enable=useless-use-of-cat if you still want it.

# .shellcheckrc
enable=useless-use-of-cat

Output: (none — exits 0 on success)

Exit codes#

0  no issues found
1  issues found at requested severity
2  fatal error (file not found, parse error)
3  bad options / usage error
shellcheck deploy.sh && echo "clean"
echo "exit=$?"

Output:

clean
exit=0

Editor and shell integration#

Most editors have a ShellCheck plugin that runs on save and surfaces findings in the gutter. The basic incantations:

# Vim — via ALE
# .vimrc
let g:ale_linters = {'sh': ['shellcheck']}

# Neovim — via nvim-lint
require('lint').linters_by_ft = { sh = {'shellcheck'}, bash = {'shellcheck'} }

# VS Code: install "ShellCheck" extension by Timon Wong

# Emacs — via flycheck
(setq flycheck-shellcheck-supported-shells '(bash dash ksh sh))

# fish completion
shellcheck --version            # uses fish's hosted completion

Output: (none — exits 0 on success)

Pre-commit hook#

A pre-commit hook catches issues before the script ever lands in the repo. The Python pre-commit framework has a ready-made hook; the manual .git/hooks/pre-commit version is below.

# .pre-commit-config.yaml — using the pre-commit framework
repos:
  - repo: https://github.com/koalaman/shellcheck-precommit
    rev: v0.11.0
    hooks:
      - id: shellcheck
        args: [--severity=warning, --external-sources]

Output: (none — exits 0 on success)

# Plain .git/hooks/pre-commit (executable)
#!/usr/bin/env bash
set -e
mapfile -t files < <(git diff --cached --name-only --diff-filter=ACM \
                       | grep -E '\.(sh|bash)$' || true)
[ "${#files[@]}" -eq 0 ] && exit 0
shellcheck -x -S warning "${files[@]}"

Output: (none — exits 0 on success)

GitHub Actions#

A two-step job that runs ShellCheck on every push and surfaces inline annotations in the PR diff.

# .github/workflows/shellcheck.yml
cat > .github/workflows/shellcheck.yml <<'YAML'
name: shellcheck
on: [push, pull_request]
jobs:
  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ludeeus/action-shellcheck@2.0.0
        with:
          severity: warning
          additional_files: 'install setup'
          ignore_paths: vendor third_party
YAML

Output: (none — exits 0 on success)

GitLab CI#

shellcheck:
  image: koalaman/shellcheck-alpine:stable
  script:
    - find . -type f \( -name '*.sh' -o -name '*.bash' \) -print0 \
        | xargs -0 shellcheck -x -S warning

Output: (none — exits 0 on success)

Common pitfalls#

  1. Disabling rules globally. If you find yourself adding disable=SC2086 to every file, the bug is in your codebase — fix the quoting instead.
  2. Pinning to latest in CI. New ShellCheck versions add rules; pin to a specific tag (v0.10.0) and bump it on purpose so CI stays reproducible.
  3. Missing shebang. Without #!/usr/bin/env bash (or equivalent), ShellCheck assumes sh and floods you with POSIX-portability warnings. Add the shebang or # shellcheck shell=bash.
  4. Treating SC2034 as gospel. Indirect variable refs (${!var}) and variables exported for child processes look unused to ShellCheck. Disable per-line, not project-wide.
  5. Forgetting -x for sourced files. Without -x, ShellCheck only sees the top-level script and misses cross-file mistakes.
  6. Not piping -f diff to patch. Many quoting fixes are auto-applicable; doing them by hand is slower and error-prone.
  7. Running shellcheck against shells it doesn’t support. It does not check fish, zsh, or PowerShell. Use fish_indent, zsh -n, and Invoke-ScriptAnalyzer respectively.

Real-world recipes#

Lint every script in the repo, fail on warnings#

A useful Makefile target or one-liner for CI:

find . -path ./vendor -prune -o \( -name '*.sh' -o -name '*.bash' \) -print0 \
  | xargs -0 shellcheck -x -S warning

Output:

(no output, exit 0 — everything clean)

Lint with a project allowlist of rules#

When migrating a legacy codebase, freeze the current rule set and only enforce what you’ve already cleaned. Add rules back as you fix them.

# .shellcheckrc — strict subset only, expand over time
shell=bash
severity=warning
disable=SC2034,SC2155,SC1091         # not ready yet
enable=quote-safe-variables

Output: (none — exits 0 on success)

Annotate findings in a PR via gcc format#

ShellCheck’s gcc output format is parsed by most CI runners and turns each finding into a clickable line in the PR diff.

shellcheck -x -f gcc -S warning ./scripts/*.sh \
  | tee shellcheck.log
# CI uploads shellcheck.log as a problem-matcher / SARIF artefact

Output:

scripts/deploy.sh:12:8: note: Double quote to prevent globbing and word splitting. [SC2086]
scripts/deploy.sh:20:1: warning: 'cd' may fail. Use 'cd ... || exit' or similar. [SC2164]

Auto-apply safe fixes across the repo#

The -f diff format produces a real patch. For small, well-scoped changes (SC2006 backticks, SC2086 quoting) this saves hours of manual work.

# Generate one combined patch
find . -name '*.sh' -print0 \
  | xargs -0 -I{} shellcheck -f diff {} >> fixes.patch
# Review what's about to change
diffstat fixes.patch
# Apply
patch -p1 < fixes.patch

Output:

 deploy.sh   |  4 ++--
 setup.sh    |  6 +++---
 backup.sh   |  2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

Lint heredoc / dotfiles / scripts without a .sh extension#

ShellCheck infers the shell from the shebang, so a script called install works fine. For files without a shebang, force the dialect.

shellcheck -s bash install
shellcheck -s bash ~/.bashrc ~/.bash_profile
shellcheck -s sh   /etc/init.d/myservice

Output:

(findings, if any)

Quiet check inside a Makefile target#

lint:
	@find . -name '*.sh' -print0 | xargs -0 shellcheck -x -S warning
	@echo "shellcheck: clean"

.PHONY: lint

Output:

$ make lint
shellcheck: clean

Test the linter itself with a deliberately broken script#

When wiring up CI, stash a known-bad script to confirm the pipeline actually fails when it should.

cat > /tmp/bad.sh <<'EOF'
#!/usr/bin/env bash
files=$(find . -name *.log)
for f in $files; do
  rm $f
done
EOF
shellcheck /tmp/bad.sh
echo "exit=$?"

Output:

In /tmp/bad.sh line 2:
files=$(find . -name *.log)
                     ^---^ SC2061 (warning): Quote the parameter to -name so the shell won't interpret it.
^----^ SC2207 (warning): Prefer mapfile or read -a to split command output ...

In /tmp/bad.sh line 3:
for f in $files; do
         ^----^ SC2086 (info): Double quote to prevent globbing and word splitting.

In /tmp/bad.sh line 4:
  rm $f
     ^-- SC2086 (info): Double quote to prevent globbing and word splitting.

exit=1

Use ShellCheck as a library from CI#

The json1 format is the cleanest input for any downstream tool — for example, posting findings as PR comments.

shellcheck -f json1 deploy.sh \
  | jq -r '.comments[] | "\(.file):\(.line):\(.column) SC\(.code) (\(.level)) \(.message)"'

Output:

deploy.sh:12:8 SC2086 (info) Double quote to prevent globbing and word splitting.
deploy.sh:20:1 SC2164 (warning) Use 'cd ... || exit' or similar to handle cd failures.

[!TIP] The single best shellcheck flag is -x (external-sources). Without it ShellCheck only sees the file you point at and misses cross-file bugs that are exactly the kind of mistake the tool was built to catch.

[!TIP] Combine shellcheck -f diff … | patch -p1 with a clean git working tree — you can preview every machine-suggested fix with git diff and roll it back instantly with git restore if any change looks wrong.

Sources#