skills/BashDevelopment/SKILL.md
Bash and POSIX shell conventions for the forge ecosystem — idioms over subprocesses, fail-safe defaults, probe guarding, multi-line command substitution, exit-code semantics. USE WHEN writing or reviewing shell scripts (audit tools, hooks, build scripts), designing CLI flag handling, or wiring shell tools into CI.
npx skillsauth add n4m3z/forge-dev BashDevelopmentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Conventions for writing bash and POSIX shell scripts in the forge ecosystem and adjacent personal projects. Reference implementation lives in check-mac and the build hooks under forge-core.
For health-check architecture (severity ladders, key:value output, orchestrator dispatch), see the companion skill HealthChecks.
Use bash built-ins instead of spawning subprocesses for things bash can do natively:
# Substring (case-sensitive)
[[ "$status" == *"enabled"* ]]
# Regex
[[ "$ports" =~ \.22[[:space:]] ]]
[[ "$dns" =~ ^(1\.1\.1\.1|8\.8\.8\.8)$ ]]
# Wildcard
[[ "$groups" == *admin* ]]
# Prefix / suffix removal
filename="${path##*/}" # basename
extension="${filename##*.}" # extension
trimmed="${var%, }" # strip trailing ", "
Avoid:
echo "$var" | grep -q "pattern" # spawns a subprocess for what bash can do natively
basename="$(basename "$path")" # use ${path##*/} instead
Use [[ ]] for tests, never [ ]. [[ ]] is bash-aware (no word splitting, supports =~, supports &&/|| inside).
Default to the safe value, then flip on observed evidence. Never default to OK:
pass_check=$CRIT
[[ "$setting" == "expected" ]] && pass_check=$OK
For values that may legitimately be empty (managed-config keys, profile-overridden plist domains), do not collapse empty into a known value:
# WRONG — masks the empty case
setting=${setting:-0}
# RIGHT — preserve the empty distinction
[[ -z "$setting" ]] && pass_check=$UNKNOWN
[[ "$setting" == "1" ]] && pass_check=$OK
[[ "$setting" == "0" ]] && pass_check=$CRIT
A probe must not provoke an interactive installer or prompt. On macOS, git --version triggers the Xcode CLT GUI installer when CLT is absent. Guard with command -v, plus xcode-select -p for Xcode-tooling probes:
git_version=""
if command -v git >/dev/null 2>&1 && xcode-select -p >/dev/null 2>&1; then
git_version=$(git --version 2>/dev/null | awk '{print $3}')
fi
Apply the command -v guard before any third-party CLI (brew, gpg, openssl). An absent tool is not a misconfiguration unless the script is specifically about the tool's installation.
For readability, break long pipelines across lines:
firewall_enabled=$(
defaults read /Library/Preferences/com.apple.alf globalstate 2>/dev/null
)
non_apple_kexts=$(
kmutil showloaded 2>/dev/null \
| awk 'NR>1 {print $NF}' \
| grep -v '^com\.apple\.'
)
Indent the body four spaces. Closing paren on its own line. Same shape works for local val=$(...):
local val
val=$(
defaults read "$DOMAIN" "$KEY" 2>/dev/null
)
Always quote variables in tests and command substitutions, except inside [[ ]] where bash does the right thing:
# Quote in shell command lines
mv "$src" "$dst"
echo "$result"
# [[ ]] does not require quoting for word-splitting, but quote for clarity
[[ "$status" == *enabled* ]]
# Always quote command substitutions in calling sites
check "$(key pass_x)" "Label" "$ENABLED" "$DISABLED"
2>/dev/null to silence stderr from probes that may legitimately fail (absent plist key, missing CLI, no matching process):
setting=$(defaults read com.apple.foo Bar 2>/dev/null)
listening=$(lsof -i :22 2>/dev/null)
Do not suppress stdout. Do not redirect to /dev/null everything wholesale — that hides bugs. Only silence stderr where empty output is the legitimate signal.
For tools that humans run interactively, exit 0 always and report status visually. For the same tool wired into CI, expose an explicit --strict flag that propagates non-zero on failure. Never make non-zero exit the default; that surprises every existing user and breaks pipes through tee or less.
strict=0
for arg in "$@"; do
case "$arg" in
--strict) strict=1 ;;
-h|--help)
echo "Usage: $0 [--strict]"
exit 0
;;
esac
done
# ... do work, accumulate $issues ...
if (( strict && issues > 0 )); then
exit 1
fi
Helpers go in a sourced lib. Standalone scripts inline what they need. Use local for function-scope variables:
key() {
local field="$1"
echo "$data" | grep "^${field}:" | cut -d: -f2-
}
cut -d: -f2- (with the trailing dash) preserves trailing colons in values; -f2 truncates at the first colon and silently corrupts IPv6 addresses, hostnames with ports, and structured status strings.
Avoid local val=$(cmd) because the assignment masks the return code of cmd. Split into two lines if the return code matters:
local val
val=$(cmd)
Run shellcheck before committing. Treat warnings as bugs unless they flag a deliberate pre-existing pattern; in that case, add a # shellcheck disable=SCxxxx comment with a one-line reason.
Common pre-existing patterns the lint may flag:
SC2155 (mask return value with local val=$(...)): split into two lines, see above.SC2154 (variable referenced but not assigned): expected for sourced libs that read variables set by the caller.SC2034 (variable appears unused): expected for status-string constants in a sourced style lib.Bash version varies. macOS ships an old bash 3.2 at /bin/bash; modern bash (4+, 5+) is at /opt/homebrew/bin/bash or /usr/local/bin/bash. Scripts using [[ ]], ${var//}, and =~ work in 3.2; associative arrays (declare -A) require 4+.
If a script needs bash 4+, use #!/usr/bin/env bash and document the dependency. If portability to old macOS bash matters, use #!/bin/bash and stay in 3.2-compatible territory.
For coreutils differences (sed, date, readlink), prefer POSIX flags or guard with command -v gsed / command -v gdate.
# Source: comment at the top of any check or audit script pointing to the upstream reference.read_hook_input).tools
Server-rendered web dashboards and apps in Rust using axum + htmx + Askama + rust-embed. USE WHEN building a web dashboard, adding a web UI to a CLI tool, server-rendered HTML, htmx partials, Askama templates, axum routes, embedded static assets, localhost webserver.
tools
Architecture for security and health-check programs: standalone-runnable checks, severity ladder with UNKNOWN, key:value output contract, orchestrator dispatch, exit-code semantics. Language-agnostic; reference implementations in Bash, applies to Python and other languages. USE WHEN writing health checks or audit tools, designing check-script contracts, adding checks to tools like check-mac, reviewing health-check architecture, or porting a check tool between languages.
testing
Scan a dotfiles tree for secrets via gitleaks, aggregate findings by top-level directory and rule, then surgically filter flagged lines from shell-history files before importing into atuin. USE WHEN auditing dotfiles before pushing to a public repo, scanning rsynced dotfiles-private contents, importing legacy zsh or bash history into atuin, filtering credential leaks out of a shell history file, deciding which dotfile subdirectories can be made public.
development
Test-driven development practices — Red-Green-Refactor cycle, test categories, coverage strategy, property-based testing. USE WHEN writing tests, designing testable APIs, or reviewing test coverage.