Why Shell Script Rules Are Critical
Shell scripts are the most dangerous code AI assistants generate. A missing quote around a variable can delete your entire filesystem (rm -rf $DIR/ when DIR is empty becomes rm -rf /). A missing error check can let a failed command go unnoticed while subsequent commands operate on stale data. And AI assistants generate both of these problems routinely.
The root cause: Bash's default behavior is to continue executing after errors, expand unquoted variables with word splitting, and silently ignore failed commands in pipelines. Every 'safe' behavior requires opting in — and AI assistants don't opt in without explicit rules.
These rules apply to Bash, sh, and Zsh scripts. They're especially important for CI/CD scripts, deployment scripts, and any script that modifies files or infrastructure.
Rule 1: Always Use Strict Mode
The rule: 'Every Bash script starts with: #!/usr/bin/env bash followed by set -euo pipefail. -e exits on any command failure. -u treats unset variables as errors. -o pipefail makes pipeline failures detectable. Never omit these — they prevent the most common classes of shell script bugs.'
What each flag does: -e (errexit) stops the script when any command returns non-zero — without it, the script keeps running after failures. -u (nounset) makes unset variable references an error — without it, $UNDEFINED silently expands to empty string. -o pipefail makes a pipeline return the exit code of the last failing command — without it, only the final command's exit code matters.
The AI frequently omits set -euo pipefail because many example scripts online don't include it. One line in your CLAUDE.md prevents entire categories of silent failures.
set -euo pipefail is non-negotiable. Without it, failed commands are silently ignored, unset variables expand to empty strings, and pipeline errors go undetected. One line prevents three categories of bugs.
Rule 2: Quote Everything
The rule: 'Always double-quote variable expansions: "$variable", "${array[@]}", "$(command)". Unquoted variables undergo word splitting and globbing — this breaks on filenames with spaces and can execute unintended commands. The only exception is inside [[ ]] where quoting is optional (but still recommended for consistency).'
The horror story: DIR="" followed by rm -rf $DIR/ expands to rm -rf / because the unquoted empty variable disappears. With quotes: rm -rf "$DIR/" safely fails on the empty path. This single quoting rule prevents the most catastrophic shell script bugs.
For arrays: 'Use "${array[@]}" (quoted, with @) to expand arrays preserving elements. Never use ${array[*]} unquoted — it merges all elements into one string with word splitting.'
- Always: "$var", "${var}", "$(cmd)", "${array[@]}"
- Unquoted variables undergo word splitting AND globbing
- Empty unquoted variable disappears — causes dangerous argument shifts
- [[ ]] is safe without quotes but quote anyway for consistency
- Use printf instead of echo for portable, safe output
DIR="" then rm -rf $DIR/ becomes rm -rf /. With quotes: rm -rf "$DIR/" safely fails on the empty path. This is not theoretical — it has happened in production.
Rule 3: Explicit Error Handling
The rule: 'Check the return value of critical commands explicitly. Use || to provide fallback behavior: command || { echo "Failed"; exit 1; }. Use trap for cleanup on exit: trap cleanup EXIT. For complex error handling, use functions that return meaningful exit codes. Log errors to stderr: echo "Error: message" >&2.'
For cleanup: 'Use trap to clean up temporary files and resources on script exit — even on failure. Pattern: trap "rm -f $tmpfile" EXIT. This runs whether the script succeeds, fails, or is interrupted with Ctrl+C.'
For conditional execution: 'Use && for commands that should only run on success. Use || for fallback behavior. Use if statements for complex conditions. Never rely on set -e alone for error handling in critical paths — it has known edge cases (functions, subshells, command substitution).'
Rule 4: Safe Patterns for Common Operations
The rule: 'Use mktemp for temporary files — never hardcode /tmp/myapp.tmp (race condition). Use readonly for constants. Use local for function variables. Use [[ ]] for conditionals — never [ ] (it's less safe and less capable). Use $() for command substitution — never backticks `. Use printf over echo for reliable output.'
For file operations: 'Use test -f / test -d before operating on files. Use mkdir -p for idempotent directory creation. Use cp/mv with -- to handle filenames starting with dashes. Use find ... -print0 | xargs -0 for safe iteration over files with special characters.'
For portability: 'Use /usr/bin/env bash in the shebang for portability. Avoid bashisms if the script needs to run on sh. Test scripts on both Linux and macOS if cross-platform. Use command -v to check for command availability instead of which.'
- mktemp for temp files — never hardcode paths
- readonly for constants — local for function variables
- [[ ]] over [ ] — $() over backticks — printf over echo
- -- after cp/mv/rm to handle dash-prefixed filenames
- find -print0 | xargs -0 for safe file iteration
- command -v over which for portability
Rule 5: ShellCheck Compliance
The rule: 'All shell scripts must pass shellcheck with zero warnings. Run shellcheck in CI alongside your other linters. Fix warnings immediately — every shellcheck warning represents a real bug or a portability issue. Use # shellcheck disable=SCXXXX only when the warning is a false positive and document why.'
ShellCheck catches: unquoted variables, useless uses of cat, unreachable code, deprecated syntax, portability issues, and dozens of other shell-specific problems. It's the most effective single tool for shell script quality.
For CI integration: 'Install shellcheck in your CI pipeline. Run it on all .sh and .bash files. Fail the build on any shellcheck error or warning. Use a .shellcheckrc file for project-wide configuration if needed.'
ShellCheck catches more shell bugs than any other tool. Run it in CI with zero-warning policy. Every warning is a real bug or portability issue — not a style preference.
Complete Shell Script Rules Template
Consolidated rules for Bash scripting.
- #!/usr/bin/env bash + set -euo pipefail — every script, no exceptions
- Double-quote all variable expansions — "$var", "$(cmd)", "${arr[@]}"
- trap cleanup EXIT — clean up temp files on any exit path
- Check critical command returns — || { echo >&2; exit 1; }
- mktemp for temp files — readonly for constants — local for function vars
- [[ ]] for conditions — $() for substitution — printf for output
- shellcheck clean — zero warnings in CI, no unwarranted disables
- Functions return exit codes — meaningful names — documented parameters