From cli
Shell script conventions, defensive patterns, and correctness rules: strict mode, quoting, portability, error handling, and common pitfalls. Invoke whenever task involves any interaction with shell scripts — writing, reviewing, debugging, or understanding .sh, .bash, .zsh files.
npx claudepluginhub xobotyi/cc-foundry --plugin cliThis skill uses the workspace's default tool permissions.
Write defensively. Shell defaults are hostile — unquoted variables split, unset variables vanish silently, failed
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Write defensively. Shell defaults are hostile — unquoted variables split, unset variables vanish silently, failed commands continue. Every rule here exists to counteract a specific shell default that causes bugs.
Extended examples, code patterns, and lookup tables for the rules below.
${CLAUDE_SKILL_DIR}/references/strict-mode.md]: errexit
caveats, pipefail examples, trap patterns, temp file safety, debugging techniques${CLAUDE_SKILL_DIR}/references/quoting.md]: Three quoting mechanisms,
"$@" vs "$*", array expansion, printf vs echo, nested quoting${CLAUDE_SKILL_DIR}/references/portability.md]: Feature comparison, GNU
vs BSD tool differences, portable pattern catalog${CLAUDE_SKILL_DIR}/references/arguments.md]: getopts template, manual
long-option parsing, validation patterns, usage messages, stdin detection${CLAUDE_SKILL_DIR}/references/pitfalls.md]: Iteration pitfalls, variable
pitfalls, test pitfalls, pipeline pitfalls, arithmetic traps${CLAUDE_SKILL_DIR}/references/builtins.md]: Parameter
expansion, replacing sed/cut/basename/expr, arrays, read patterns, arithmeticEvery bash script starts with:
#!/usr/bin/env bash
set -euo pipefail
#!/usr/bin/env bash — not #!/bin/bash. The env lookup is more portable across systems where
bash is not at /bin/bash.set -e (errexit): Exit on command failure. Understand the exceptions: commands in if/while conditions, left
side of &&/||, and negated commands (!) do not trigger errexit.set -u (nounset): Error on unset variables. Use ${VAR:-default} for optional variables.set -o pipefail: Pipeline returns the rightmost failing command's exit code, not the last command's.#!/bin/sh. Drop pipefail (not POSIX). Use set -eu with caution — set -e behavior
varies across sh implementations.#!/usr/bin/env bash
set -euo pipefail
#
# deploy.sh — Build and deploy the application to staging.
Quoting is the single most important discipline. Unquoted variables undergo word splitting (breaks on IFS characters) and pathname expansion (glob characters match filenames). Both are silent and devastating.
"$var", "${var}"."$(command)"."$@" to pass arguments through. Never $* or $@ unquoted. "$@" preserves each argument as a separate
word. "$*" joins them."${arr[@]}" expands each element as a separate word. Unquoted ${arr[@]} undergoes word
splitting.for f in *.txt — the glob must expand. But always quote variables inside the loop: "$f".[[ ]] right-hand patterns unquoted when doing glob or regex matching. Quote the right side for literal
string comparison.grep 'pattern' file.printf instead of echo for data output. echo interprets -n, -e as options on some platforms.
printf '%s\n' "$var" is always safe.var=$other (no splitting in assignment context)(( )) arithmetic: (( x + y ))[[ ]] on the left side: [[ $var == pattern ]]$?, $#, $$ (guaranteed no spaces)case word: case $var in ...file_path, line_count). UPPER_CASE for
exported/environment variables and constants (PATH, MAX_RETRIES).readonly:
readonly CONFIG_DIR="/etc/myapp"
local in functions to prevent variable leakage into global scope. Declare and assign on separate lines when
capturing command output:
local result
result=$(some_command)
Combined local result=$(cmd) masks the exit code — local always returns 0.${VAR:-default} to provide defaults without modifying the variable. Use ${VAR:=default} to
set and use.${VAR:?error message} to abort if unset.files=("file one.txt" "file two.txt")
command "${files[@]}"
|| exit 1, || return 1, or explicit if blocks. Especially cd,
mkdir, rm, cp, mv.
cd "$dir" || exit 1
trap on EXIT for reliable cleanup:
tmpfile=$(mktemp) || exit 1
trap 'rm -f "$tmpfile"' EXIT
mktemp for temp files. Never hardcoded temp paths. Always clean up via trap.die() { printf '%s\n' "$1" >&2; exit "${2:-1}"; }
set -e as a substitute for error handling. It has many edge cases. Use it as a safety net, but still
check critical commands explicitly.name() { ... } — no function keyword (it's not POSIX and adds nothing in bash).local for all function variables. Bash functions share the caller's scope by default — every undeclared
variable is global.local declaration from command substitution:
my_func() {
local output
output=$(some_command) || return 1
}
set statements, source commands, and constants should precede
function definitions.main for scripts with multiple functions. Call main "$@" as the last line. This keeps the entry point
obvious and lets all variables be local.
main() {
local arg="$1"
# ...
}
main "$@"
[[ ]] in bash — it prevents word splitting, supports &&/|| inside the test, and enables pattern/regex
matching. In POSIX sh, use [ ] with all variables quoted.(( )) for numeric comparisons:
if (( count > 10 )); then ...
In POSIX sh: [ "$count" -gt 10 ].== in [[ ]] and = in [ ] for string equality.[[ -z "$var" ]] and [[ -n "$var" ]] — not [[ "$var" ]].&&/|| as if/then/else:
# WRONG — cmd3 runs if cmd2 fails, even when cmd1 succeeds
cmd1 && cmd2 || cmd3
# RIGHT
if cmd1; then cmd2; else cmd3; fi
ls output. Use globs:
for f in ./*.txt; do
[[ -e "$f" ]] || continue
process "$f"
done
while read for line-oriented input:
while IFS= read -r line; do
printf '%s\n' "$line"
done < file
The IFS= prevents leading/trailing whitespace trimming. The -r prevents backslash interpretation.while IFS= read -r line; do
(( count++ ))
done < <(command)
echo "$count" # preserved
find -print0 with read -d '' for filenames with special characters:
while IFS= read -r -d '' file; do
process "$file"
done < <(find . -type f -print0)
case for multi-way branching:
case "$1" in
start) do_start ;;
stop) do_stop ;;
restart) do_stop; do_start ;;
*) die "Unknown command: $1" ;;
esac
case. Put ;; on the same line as the action for one-liners, on its own line
for multi-line actions.getopts for short options. It is POSIX, handles combined flags (-vf), and manages OPTARG/OPTIND
correctly.while (( $# > 0 )); do case loop with explicit -- handling.-- to end option processing — prevents filenames starting with - from being interpreted as
options.-- when passing variables to commands:
rm -- "$file"
grep -- "$pattern" "$file"
./ to prevent files named -rf from becoming options:
for f in ./*; do
rm -- "$f"
done
usage() function for any script that takes arguments. Print to stderr and exit with code 64
(EX_USAGE).[[ -t 0 ]] tests whether stdin is a terminal.<<- heredocs).\ continuation or heredocs for long strings.; then and ; do on the same line as if/for/while:
if [[ -f "$file" ]]; then
for item in "${arr[@]}"; do
while read -r line; do
| on the continuation line:
command1 \
| command2 \
| command3
$(command) not backticks. $() nests cleanly and is readable. Backticks require escaping and don't nest.${var} braces for all variables except positional parameters ($1-$9) and special parameters ($?,
$#, etc.).#!/usr/bin/env bash, use [[ ]], arrays, and process substitution freely. Specify
minimum bash version if using 4.0+ features (associative arrays, mapfile, case modification).#!/bin/sh, use [ ] with quoted variables, no arrays, no [[ ]], no (( )), no
local (technically non-POSIX but widely supported), no process substitution.sed -i, grep -P, GNU date flags. Document the
dependency when GNU tools are required.command -v to check if a program is available — not which (which is not a builtin and behaves differently
across systems).# shellcheck disable=SC2086
word_split_is_intentional $var
# shellcheck shell=bash
cd ... || exit in case cd failsWhen writing shell scripts: Apply all rules silently. Produce clean, defensive code. Use strict mode, quote everything, handle errors, use arrays for lists.
When reviewing shell scripts: Cite the specific rule violated. Show the fix inline. Prioritize: quoting bugs > error handling gaps > style issues.
the-coder provides the overall coding workflow (discover, plan, verify)Quote everything. Handle every error. Trust nothing.