From shell-routines
Write secure, portable bash scripts with proper structure, error handling, and quoting. Use when scaffolding a new script ("write a bash script", "scaffold", "new shell file") or modifying, auditing, or hardening an existing one ("fix this script", "refactor bash", "add error handling to"). Covers bash functions, helpers, deployment scripts, and file operations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/shell-routines:shell-best-practicesThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Guides Claude to write secure, portable, well-structured bash scripts — and to scaffold new scripts from the right template when starting from scratch.
Guides Claude to write secure, portable, well-structured bash scripts — and to scaffold new scripts from the right template when starting from scratch.
Mode A — Modify/improve: Apply the core standards below to the existing script at $ARGUMENTS.
Mode B — New script: Select the appropriate template (see Templates section), create the file at $ARGUMENTS (see naming rules in Scaffolding Process), fill in the placeholders, then apply core standards.
Every script must include:
#!/usr/bin/env bash
set -euo pipefail
#!/usr/bin/env bash — not /bin/bashset -euo pipefail — catches unbound variables, failed commands, and pipe errorsset -e does not exit on failures inside if/while conditions, &&/|| chains, subshells, or negated commands. Use explicit exit-code checks or trap ERR for reliable error handling:
trap 'echo "Error at line $LINENO" >&2; exit 1' ERR
"$var", "${array[@]}""$(cmd)"Local variables: lower_case_with_underscores
Constants and globals/exports: UPPERCASE_WITH_UNDERSCORES
Public functions: <namespace>::function_name — use shroutines:: for plugin-internal scripts, or the project name (e.g. myapp::) for project-specific scripts
Private functions: _function_name — leading underscore signals internal use; not part of any public API
Use local -r for constants inside functions — scopes the variable and protects it. Use readonly for script-level constants (maximum portability). Use declare -r at the top level only when you need type flags (-i, -a, -lx). The meaningful distinction is local -r (scoped) vs readonly/declare -r (global):
# Script-level — readonly for portability
SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_NAME
readonly VERSION="0.1.0"
# Script-level with type flag — declare -r
declare -ar EXIT_CODES=(["ok"]=0 ["error"]=1 ["usage"]=2)
# Function-scoped — local -r
_parse_args() {
local -r max_retries=3
}
# Public function — shroutines:: namespace
shroutines::process_file() {
local input="$1"
# ...
}
Replace the
shroutines::prefix with the target project's namespace when scaffolding scripts for external projects.
shroutines::process_file() {
local input_path="$1"
if [[ ! -r "$input_path" ]]; then
echo "Error: cannot read file: $input_path" >&2
return 1
fi
# logic here
return 0
}
Use local for all function-scoped variables
Separate declaration and assignment when the value comes from a command substitution — local does not propagate the exit code:
# BAD - $? is always 0 (exit code of 'local', not my_func)
local my_var="$(my_func)"
(( $? == 0 )) || return
# GOOD - separate lines preserve the exit code
local my_var
my_var="$(my_func)"
(( $? == 0 )) || return
End functions with explicit return 0 — makes success exit point visible and distinguishes "fell off the end" from "deliberately succeeded"
Use [[ ]] for bash tests, [ ] for POSIX sh
Errors go to stderr: >&2
Return meaningful exit codes: 0 = success, 1 = error, 2 = misuse
Every script must follow this top-to-bottom order for any sections that are present:
#!/usr/bin/env bash + set -euo pipefailreadonly / declare -r values that never changeUPPERCASE variables with script-wide scope_function_name helpers, internal utilitiesshroutines::function_name entry points and APIBASH_SOURCE[0] guard + main "$@"#!/usr/bin/env bash
set -euo pipefail
readonly VERSION="0.1.0"
VERBOSE=0
OUTPUT_FILE=""
_log_info() { printf '[INFO] %s\n' "$*" >&2; return 0; }
shroutines::process() {
local input="$1"
return 0
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
Soft limit: 120 characters per line
Break long strings, pipelines, or argument lists across lines
Prefer printf over echo for multi-line output
Do not use && ... || ... as if/else — if the middle command fails, the || branch runs even though the && condition succeeded:
# BAD - rollback runs if deploy succeeds but healthcheck fails
deploy && healthcheck || rollback
# GOOD - explicit if/else
if deploy && healthcheck; then
echo "Deploy succeeded"
else
rollback
fi
Only comment when the code itself cannot convey the information: a hidden constraint, a subtle invariant, a bug workaround, or behaviour that would surprise a careful reader. Write one explaining why not what. If removing the comment wouldn't confuse a future reader, don't write it.
File header — every script starts with one:
#!/usr/bin/env bash
#
# [BRIEF DESCRIPTION OF WHAT THIS SCRIPT DOES]
# Usage: [SCRIPT_NAME] [ARGUMENTS]
#
Section dividers — only when an individual section exceeds ~50 lines or complexity makes structure worth signposting. The standard file structure ordering is self-evident; dividers between short sections add noise. # tail fills to the nearest of 40, 80, or 120 columns:
###
### :::: [description] :::: ###########
###
Public function docs — only when the function's name and arguments don't fully convey its contract: non-obvious return codes, argument constraints, side effects, or failure conditions. Public (shroutines::) functions only; never on private (_) helpers.
# [description]
# Arguments:
# $1 - [name]: [description]
# $2 - [name]: [description]
# Returns:
# 0 - [success description]
# 1 - [failure description]
Inline comments — trailing # on the same line:
# GOOD — explains why
local -r threshold=$((mem_total / 10)) # 10% of total memory
# BAD — restates what
local -r threshold=$((mem_total / 10)) # calculate threshold
Annotation comments — bare # line above a block:
# Track descriptors so the trap can close them even if the list changes later
exec 3>/var/log/daemon.log
exec 4>&1
_OPEN_FDS+=(3 4)
eval — command injection risksh or bash — injection riskFor input validation, secrets handling, temp-file hygiene, and file permissions, see references/security.md.
Choose based on complexity and purpose:
| Template | Use When |
|---|---|
assets/standard.sh | Most scripts — argument parsing, error handling, direct execution |
assets/minimal.sh | Simple one-task utilities, no complex flag parsing |
assets/library.sh | Sourced by other scripts; provides reusable functions, no direct execution |
assets/posix.sh | POSIX sh — containers, embedded, Alpine, CI base images, maximum portability |
--flag style options or multiple arguments? → standardsource it? → library$ARGUMENTS or ask the user$ARGUMENTS — for directly executed scripts, omit the .sh extension (e.g. deploy not deploy.sh); for libraries meant to be sourced, keep the .sh extension (e.g. lib-common.sh)#!/bin/sh shebang, and follow the POSIX sh Feature Restrictions below. Run checkbashisms to verify the final resultDone when: the automated ShellCheck and shfmt hooks report clean on the scaffolded file — and checkbashisms for #!/bin/sh scripts. Resolve every finding before considering the script complete.
This plugin targets Bash 4.4+ as its primary compatibility tier. POSIX sh is a secondary tier for maximum portability.
Rule: If the shebang says #!/bin/sh, the script must contain zero bashisms. Use #!/usr/bin/env bash if you need bash features. The hook pipeline detects the shebang and configures ShellCheck, shfmt, and checkbashisms accordingly.
| Shebang | Meaning | Tooling |
|---|---|---|
#!/usr/bin/env bash | Bash-specific. Bashisms allowed. | shellcheck -s bash, shfmt -ln bash, bash -n |
#!/bin/sh | POSIX sh only. No bash features. | shellcheck -s sh, shfmt -ln posix, checkbashisms |
#!/usr/bin/dash | Explicit dash. Same as POSIX sh. | shellcheck -s dash, shfmt -ln posix, checkbashisms |
Choose #!/bin/sh when:
Choose #!/usr/bin/env bash when:
[[ ]]For POSIX sh scripts, ensure no bashisms are present. Common traps: [[ ]], arrays, ${var,,}, <<<, source, function keyword, echo -e. Use checkbashisms to verify.
When the user specifies POSIX portability, use assets/posix.sh instead of the bash templates.
references/patterns.md — Argument parsing, temp files, arrays, string manipulation, parallel processing, progress output, exit code handlingreferences/security.md — Preventive security patterns for writing: injection prevention, input validation, temp files, signal handling${CLAUDE_PLUGIN_ROOT}/scripts/lib-common.sh — General-purpose runtime library (logging, validation, temp files, string/array utilities), all functions shroutines::-namespaced. Source directly when the script can depend on the plugin being installedAlways consult these reference files before producing output — both for reviewing existing scripts and scaffolding new ones.
shell-security — Destructive commands, credential exposure, system file risksshell-review — Structured quality review of a completed scriptshell-architect agent — Multi-file project design, performance decisions, library vs executable structurenpx claudepluginhub objctp/shell-routinesBlocks Edit/Write/Bash actions until Claude investigates importers, data schemas, and user instructions. Improves output quality by forcing concrete facts before edits.