$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Best Practices

AI Rules for Shell Scripts and Bash

AI-generated shell scripts are the most dangerous code in your repo. Rules for quoting, error handling, shellcheck compliance, and safe patterns.

7 min read·July 18, 2024

A missing quote in a shell script can delete your entire filesystem

set -euo pipefail, quote everything, shellcheck in CI — non-negotiable rules

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.

⚠️ Non-Negotiable

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
💡 Quote Everything

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 in CI

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