View as MarkdownUpdated 2026-09

The shell stack we would set up today

Four tools that make shell scripting survivable, and the point at which you should stop writing shell entirely.

Shell is the language you write least deliberately and run most often. A small amount of tooling changes it from a source of 3am surprises into something you can trust.

Install these#

shellcheck — non-negotiable#

The single highest-value tool in this ecosystem. It catches unquoted expansions, wrong test operators, useless cat, subshell variable loss, and about three hundred other things — including nearly every mistake in generated shell.

shell
brew install shellcheck        # or apt install shellcheck
shellcheck -S warning scripts/*.sh

Wire it into your agent's post-edit hook and generated scripts get fixed before you see them. See shell scripts as agent guardrails.

shfmt#

Formatting, so diffs are about substance.

shell
shfmt -w -i 2 -ci -bn scripts/

bats for anything important#

If a script does something you would be upset to get wrong, test it.

bats test/deploy.bats
@test "refuses to deploy without a target" {
  run ./deploy.sh
  [ "$status" -eq 2 ]
  [[ "$output" == *"usage"* ]]
}

trash instead of rm#

shell
brew install trash

Then alias rm to it in your interactive shell. Not in scripts — in your shell, where the accidents happen. Recoverable deletes cost nothing and have saved a great many afternoons.

Modern replacements worth having#

None of these are required and all of them make the terminal better. All free.

OldNewWhy
grepripgrep (rg)much faster, respects .gitignore
findfdsane syntax, fast
catbatsyntax highlighting, paging
lsezagit status in the listing
cdzoxidejumps to frecent directories
sed/awk for JSONjqthe right tool, and agents use it correctly
sed/awk for YAMLyqsame
dudustreadable output

jq deserves particular mention: it is how agent hooks parse their input, and generated jq is generally correct because the language is small.

Terminal#

Warp's case is the block model — each command and its output are a discrete unit, which makes agent-driven sessions far easier to follow and to scroll back through. There is a good free tier.

Ghostty, WezTerm, Alacritty and Kitty are all excellent, all free, and we earn nothing from any of them. If you are happy with your terminal, this is not a problem worth solving.

Where to run it#

For a box to run scripts, cron jobs and self-hosted agent runners on, Hetzner is the price-performance answer and has been for years.

Droplets if you want the better dashboard, the managed databases and the $200 of credit to experiment with.

When to stop writing shell#

This is the most useful advice on the page.

Stop at about 100 lines, or at the first of these:

  • You need an array of anything other than strings
  • You need to parse JSON that is more than one jq call
  • You need error handling more subtle than "exit non-zero"
  • You are writing a function that returns a value
  • You have written eval

At that point, Python or Go will be shorter, clearer, testable, and much easier to review. A 400-line bash script is a program written in a language that does not have data structures — and generated shell at that length is genuinely hard to verify.

The shell's job is orchestration: run these commands, in this order, stop if one fails. It is very good at that, and it should mostly do only that.

The script header, one more time#

shell
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
trap 'echo "failed at line $LINENO: $BASH_COMMAND" >&2' ERR

If you take one thing from this site, take those four lines.

Common questions#

Is set -e enough on its own?#

No — it has well-known gaps, particularly in pipelines and inside conditionals. set -Eeuo pipefail together with an ERR trap covers far more, and set -u in particular prevents the unset-variable-in-a-path accident that causes the worst outcomes.

bash or POSIX sh?#

bash unless you genuinely need to run on Alpine or a BSD without it, in which case sh and shellcheck -s sh to enforce it. Writing POSIX-only by default costs you arrays and [[ ]] for a portability you probably do not need.

Should I use zsh or fish for scripts?#

No. Use them interactively if you like them; write scripts in bash with an explicit shebang. Scripts get run by things that are not your shell.

Get the Bash agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Bash. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.

Disclosure: some links on this page are affiliate links. If you buy something through one, we earn a commission at no extra cost to you. We only list tools we would tell a friend to use, and we say so when we have not used something ourselves. This is how the site stays free and ad-light.