cheat sheet

shellcheck

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.

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.

bash
# 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:

text
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.

bash
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.

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

Output:

text
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.
bash
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:

text
# -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.

bash
#!/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.

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

Output (--list-optional):

text
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.

text
# .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)

bash
# 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.

bash
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:

text
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.

bash
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):

text
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):

text
{
  "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):

text
--- 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.

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

Output:

text
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.

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

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

Output:

text
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.

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

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

Output:

text
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.

bash
# 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:

text
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.

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

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

Output:

text
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.

bash
# 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:

text
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.

bash
# Bad
mycmd $@

# Good
mycmd "$@"

Output:

text
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.

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

Output:

text
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.

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

Output:

text
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.

bash
# Bad
ver=`git describe --tags`

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

Output:

text
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.

bash
# 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:

text
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 )).

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

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

Output:

text
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.

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

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

Output:

text
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.

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

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

Output:

text
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.

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

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

Output:

text
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.

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

Output: (none — exits 0 on success)

Exit codes

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

Output:

text
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:

bash
# 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.

bash
# .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)

bash
# 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.

bash
# .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

yaml
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:

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

Output:

text
(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.

bash
# .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.

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

Output:

text
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.

bash
# 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:

text
 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.

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

Output:

text
(findings, if any)

Quiet check inside a Makefile target

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

.PHONY: lint

Output:

text
$ 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.

bash
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:

text
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.

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

Output:

text
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.

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.

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