shell-scripting skill
✓ VerifiedProvides defensive programming patterns, strict mode configurations, and correctness rules for writing portable bash and POSIX sh scripts.
- GitHub stars
- 23
- Files
- 8
- Last commit
- Nov 11, 2026
- License
- MIT
- Allowed tools
- —
What does the shell-scripting skill do?
shell-scripting provides defensive programming patterns, strict mode configurations, and correctness rules for writing portable bash and POSIX sh scripts.
Summary generated from the skill's SKILL.md.
About the shell-scripting skill
Writing reliable shell scripts requires navigating hostile defaults like silent variable failures and unquoted word splitting. This skill provides developers with defensive programming patterns and correctness rules to ensure scripts are portable and bug-free. It is designed for anyone writing, reviewing, or debugging .sh, .bash, or .zsh files, offering specific guidance on strict mode configurations, quoting disciplines, and error handling.
When an agent loads this skill, it can apply best practices for both bash and POSIX sh environments. It relies on a structured set of reference files covering topics such as argument parsing with getopts, trap patterns for cleanup, and the differences between GNU and BSD tools. The skill helps prevent common pitfalls like parsing ls output or using echo instead of printf, ensuring that scripts remain stable across different systems like macOS and Linux.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the shell-scripting skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the shell-scripting skill in Plugin
/plugin marketplace add xobotyi/cc-foundry /plugin install cli@foundry
Install the shell-scripting skill in Claude Code
~/.claude/skills/shell-scripting/SKILL.md
$ npx degit xobotyi/cc-foundry/plugins/cli/skills/shell-scripting ~/.claude/skills/shell-scriptingInstall the shell-scripting skill in Claude Code (project)
.claude/skills/shell-scripting/SKILL.md
$ npx degit xobotyi/cc-foundry/plugins/cli/skills/shell-scripting .claude/skills/shell-scriptingInstall the shell-scripting skill in Codex
~/.agents/skills/shell-scripting/SKILL.md
$ npx degit xobotyi/cc-foundry/plugins/cli/skills/shell-scripting ~/.agents/skills/shell-scriptingInstall the shell-scripting skill in Gemini CLI
~/.gemini/skills/shell-scripting/SKILL.md
$ npx degit xobotyi/cc-foundry/plugins/cli/skills/shell-scripting ~/.gemini/skills/shell-scriptingInstall the shell-scripting skill in Claude.ai
npx degit xobotyi/cc-foundry/plugins/cli/skills/shell-scripting shell-scripting && zip -r shell-scripting.zip shell-scripting # claude.ai → Customize → Skills → + → Create skill → Upload a skill → shell-scripting.zip
Automated checks
Ran Nov 14, 2026Full report →Passed our automated checks. That is not a security audit: read the scripts before you let an agent run them. How checks work
- Source repository linked and reachablexobotyi/cc-foundry/plugins/cli/skills/shell-scripting is public and reachablegithub.com/xobotyi/cc-foundry/tree/HEAD/plugins/cli/skills/s ↗Pass
- Open source license (OSI)MITspdx.org/licenses/MIT.html ↗Pass
- Commit or release in the last yearLast commit 2026-11-11github.com/xobotyi/cc-foundry/commits ↗Pass
- No download-and-run or credential theft in scriptsNo scripts in the skill folderPass
- No hidden instructions in SKILL.mdNo override phrases, safeguard switches, credential requests or hidden charactersgithub.com/xobotyi/cc-foundry/blob/HEAD/plugins/cli/skills/s ↗Pass
- Pre-approved tools are scopedNo pre-approved tools; the agent asks before using onegithub.com/xobotyi/cc-foundry/blob/HEAD/plugins/cli/skills/s ↗Pass
- SKILL.md follows the Agent Skills formatValid name and description; says when to use it; 352 linesgithub.com/xobotyi/cc-foundry/blob/HEAD/plugins/cli/skills/s ↗Pass
SKILL.md
View on GitHubShell Scripting
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.
References
Extended examples, code patterns, and lookup tables for the rules below.
- Strict mode, error handling, traps, debugging — [
${CLAUDE_SKILL_DIR}/references/strict-mode.md]: errexit caveats, pipefail examples, trap patterns, temp file safety, debugging techniques - Quoting rules, word splitting, globbing — [
${CLAUDE_SKILL_DIR}/references/quoting.md]: Three quoting mechanisms,"$@"vs"$*", array expansion, printf vs echo, nested quoting - POSIX sh vs bash, portable constructs — [
${CLAUDE_SKILL_DIR}/references/portability.md]: Feature comparison, GNU vs BSD tool differences, portable pattern catalog - Argument parsing, getopts, validation — [
${CLAUDE_SKILL_DIR}/references/arguments.md]: getopts template, manual long-option parsing, validation patterns, usage messages, stdin detection - Common shell scripting mistakes — [
${CLAUDE_SKILL_DIR}/references/pitfalls.md]: Iteration pitfalls, variable pitfalls, test pitfalls, pipeline pitfalls, arithmetic traps - Pure bash/sh alternatives to external commands — [
${CLAUDE_SKILL_DIR}/references/builtins.md]: Parameter expansion, replacing sed/cut/basename/expr, arrays, read patterns, arithmetic
Script Header
Every bash script starts with:
#!/usr/bin/env bash
set -euo pipefail
- Shebang: Use
#!/usr/bin/env bash— not#!/bin/bash. Theenvlookup is more portable across systems where bash is not at/bin/bash. set -e(errexit): Exit on command failure. Understand the exceptions: commands inif/whileconditions, 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.- For POSIX sh scripts: Use
#!/bin/sh. Droppipefail(not POSIX). Useset -euwith caution —set -ebehavior varies across sh implementations. - File header: A script's purpose goes in its name and its
--helptext, not a header comment. Add one only when the name cannot carry it and the script has no--help.
#!/usr/bin/env bash
set -euo pipefail
#
# deploy.sh — Build and deploy the application to staging.
Quoting
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.
Core Rules
- Always double-quote variable expansions:
"$var","${var}". - Always double-quote command substitutions:
"$(command)". - Use
"$@"to pass arguments through. Never$*or$@unquoted."$@"preserves each argument as a separate word."$*"joins them. - Quote array expansions:
"${arr[@]}"expands each element as a separate word. Unquoted${arr[@]}undergoes word splitting. - Leave globs unquoted:
for f in *.txt— the glob must expand. But always quote variables inside the loop:"$f". - Leave
[[ ]]right-hand patterns unquoted when doing glob or regex matching. Quote the right side for literal string comparison. - Use single quotes for literal strings that need no expansion:
grep 'pattern' file. - Use
printfinstead ofechofor data output.echointerprets-n,-eas options on some platforms.printf '%s\n' "$var"is always safe.
When Quoting Is Not Needed
- Right side of assignment:
var=$other(no splitting in assignment context) - Inside
(( ))arithmetic:(( x + y )) - Inside
[[ ]]on the left side:[[ $var == pattern ]] - Integer special variables:
$?,$#,$$(guaranteed no spaces) caseword:case $var in ...
Variable Handling
-
Naming: lowercase with underscores for local variables (
file_path,line_count). UPPER_CASE for exported/environment variables and constants (PATH,MAX_RETRIES). -
Declare constants with
readonly:readonly CONFIG_DIR="/etc/myapp" -
Use
localin 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 —localalways returns 0. -
Default values: Use
${VAR:-default}to provide defaults without modifying the variable. Use${VAR:=default}to set and use. -
Required variables: Use
${VAR:?error message}to abort if unset. -
Arrays for lists: Use bash arrays instead of space-delimited strings.
files=("file one.txt" "file two.txt") command "${files[@]}"
Error Handling
-
Check every command that can fail. Use
|| exit 1,|| return 1, or explicitifblocks. Especiallycd,mkdir,rm,cp,mv.cd "$dir" || exit 1 -
Trap for cleanup. Use
trapon EXIT for reliable cleanup:tmpfile=$(mktemp) || exit 1 trap 'rm -f "$tmpfile"' EXIT -
Use
mktempfor temp files. Never hardcoded temp paths. Always clean up via trap. -
Error messages to stderr:
die() { printf '%s\n' "$1" >&2; exit "${2:-1}"; } -
Exit codes: Return 0 for success, non-zero for failure. Use meaningful codes: 1 for general error, 2 for usage error, 64+ for application-specific errors (following sysexits convention).
-
Never use
set -eas a substitute for error handling. It has many edge cases. Use it as a safety net, but still check critical commands explicitly.
Functions
-
Declare with
name() { ... }— nofunctionkeyword (it's not POSIX and adds nothing in bash). -
Use
localfor all function variables. Bash functions share the caller's scope by default — every undeclared variable is global. -
Return values via exit code (0 = success, non-zero = failure) or via stdout. Never rely on global variables for function output.
-
Separate
localdeclaration from command substitution:my_func() { local output output=$(some_command) || return 1 } -
Put all functions before executable code. Only
setstatements, source commands, and constants should precede function definitions. -
Use
mainfor scripts with multiple functions. Callmain "$@"as the last line. This keeps the entry point obvious and lets all variables be local.main() { local arg="$1" # ... } main "$@"
Control Flow
Conditionals
-
Use
[[ ]]in bash — it prevents word splitting, supports&&/||inside the test, and enables pattern/regex matching. In POSIX sh, use[ ]with all variables quoted. -
Use
(( ))for numeric comparisons:if (( count > 10 )); then ...In POSIX sh:
[ "$count" -gt 10 ]. -
Use
==in[[ ]]and=in[ ]for string equality. -
Test empty/non-empty explicitly:
[[ -z "$var" ]]and[[ -n "$var" ]]— not[[ "$var" ]]. -
Never use
&&/||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
Loops
-
Never parse
lsoutput. Use globs:for f in ./*.txt; do [[ -e "$f" ]] || continue process "$f" done -
Use
while readfor line-oriented input:while IFS= read -r line; do printf '%s\n' "$line" done < fileThe
IFS=prevents leading/trailing whitespace trimming. The-rprevents backslash interpretation. -
Use process substitution to avoid subshell variable loss:
while IFS= read -r line; do (( count++ )) done < <(command) echo "$count" # preserved -
Use
find -print0withread -d ''for filenames with special characters:while IFS= read -r -d '' file; do process "$file" done < <(find . -type f -print0)
Case Statements
-
casefor multi-way branching:case "$1" in start) do_start ;; stop) do_stop ;; restart) do_stop; do_start ;; *) die "Unknown command: $1" ;; esac -
Indent patterns by 2 spaces from
case. Put;;on the same line as the action for one-liners, on its own line for multi-line actions.
Input Handling
-
Use
getoptsfor short options. It is POSIX, handles combined flags (-vf), and managesOPTARG/OPTINDcorrectly. -
Use manual parsing for long options.
while (( $# > 0 )); do caseloop with explicit--handling. -
Always handle
--to end option processing — prevents filenames starting with-from being interpreted as options. -
Always use
--when passing variables to commands:rm -- "$file" grep -- "$pattern" "$file" -
Prefix globs with
./to prevent files named-rffrom becoming options:for f in ./*; do rm -- "$f" done -
Provide a
usage()function for any script that takes arguments. Print to stderr and exit with code 64 (EX_USAGE). -
Validate arguments early. Check counts, types, file existence before starting work.
-
Detect stdin vs terminal:
[[ -t 0 ]]tests whether stdin is a terminal.
Formatting
-
Indent with 2 spaces. No tabs (except in
<<-heredocs). -
Maximum line length: 80 characters. Use
\continuation or heredocs for long strings. -
; thenand; doon the same line asif/for/while:if [[ -f "$file" ]]; then for item in "${arr[@]}"; do while read -r line; do -
Split long pipelines one per line with
|on the continuation line:command1 \ | command2 \ | command3 -
Use
$(command)not backticks.$()nests cleanly and is readable. Backticks require escaping and don't nest. -
Prefer
${var}braces for all variables except positional parameters ($1-$9) and special parameters ($?,$#, etc.).
Portability
- Choose your target. Decide upfront whether you need POSIX sh compatibility or can require bash.
- If targeting bash: use
#!/usr/bin/env bash, use[[ ]], arrays, and process substitution freely. Specify minimum bash version if using 4.0+ features (associative arrays,mapfile, case modification). - If targeting POSIX sh: use
#!/bin/sh, use[ ]with quoted variables, no arrays, no[[ ]], no(( )), nolocal(technically non-POSIX but widely supported), no process substitution. - macOS ships bash 3.2 permanently. If targeting macOS without requiring Homebrew bash, avoid bash 4+ features.
- Avoid GNU-specific tool options when portability matters:
sed -i,grep -P, GNUdateflags. Document the dependency when GNU tools are required. - Use
command -vto check if a program is available — notwhich(which is not a builtin and behaves differently across systems).
ShellCheck Integration
-
Run ShellCheck on all scripts. It catches quoting errors, portability issues, and common pitfalls automatically.
-
Use a directive comment for intentional violations:
# shellcheck disable=SC2086 word_split_is_intentional $var -
Specify shell dialect if the shebang is absent or ambiguous:
# shellcheck shell=bash -
Common ShellCheck codes to know:
- SC2086: Double quote to prevent globbing and word splitting
- SC2046: Quote this to prevent word splitting
- SC2034: Variable appears unused (might be exported or sourced)
- SC2155: Declare and assign separately to avoid masking return values
- SC2164: Use
cd ... || exitin casecdfails
Application
When 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.
Integration
the-coderprovides the overall coding workflow (discover, plan, verify)- Language plugins (golang, javascript) handle language-specific tooling
- This skill handles shell-specific correctness and defensive patterns
Quote everything. Handle every error. Trust nothing.
Files in this skill
8shell-scripting skill questions
How do I install the shell-scripting skill in Claude Code?
Run npx degit xobotyi/cc-foundry/plugins/cli/skills/shell-scripting ~/.claude/skills/shell-scripting. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the shell-scripting skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
What is the recommended shebang for bash scripts?
Use #!/usr/bin/env bash instead of #!/bin/bash to ensure better portability across different systems.
How can I ensure a script exits immediately when a command fails?
You can use set -e (errexit) to exit on command failure, though you should still explicitly check critical commands.
How should I handle temporary files safely?
Use the mktemp command to create temporary files and use a trap on EXIT to ensure they are cleaned up.