Skip to content
CloudOps
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 11 min read

Hardening a Bash Script with AI: Strict Mode, Traps, and Back-Out

Use AI to turn a fragile bash script into a production-grade one — strict mode, error traps, cleanup handlers, and a back-out path you can trust under load.

  • #bash
  • #python
  • #error-handling
  • #production

I inherited a 40-line deploy script that had taken down production twice. No set -e, no cleanup, a cd that silently failed and then rm -rf ran in the wrong directory. Instead of rewriting it from scratch, I fed it to an AI assistant section by section and asked it to harden each piece. The result wasn’t magic — it was a fast junior engineer producing a competent first draft that I reviewed line by line before it touched a server again. That review step is non-negotiable, and I’ll show you exactly why.

Start by asking the AI to explain the blast radius

Before changing anything, I paste the script and ask: “What can go wrong with this script if it fails halfway through? List the worst outcomes.” This is a great use of AI because it reads the whole thing faster than I do and surfaces failure modes I’d skim past.

For my deploy script it flagged the unguarded cd, an rm -rf "$BUILD_DIR/*" where $BUILD_DIR could be empty, and the fact that a failed tar extraction left a half-populated release directory marked as “current.” None of those were obvious at a glance. The AI didn’t fix anything yet — it gave me a map. I treat that map as a hypothesis list, not gospel, and confirm each one myself.

Apply strict mode, but understand every flag

The first concrete change is the unofficial bash strict mode. The AI will produce this in a second, but you need to know what each line does before you accept it.

#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
  • -e exits on any unhandled non-zero return.
  • -u errors on unset variables, killing the empty-$BUILD_DIR bug.
  • -o pipefail makes cmd1 | cmd2 fail if cmd1 fails, not just cmd2.
  • -E makes ERR traps fire inside functions and subshells.
  • The narrowed IFS stops word-splitting on spaces, a common source of mangled filenames.

Pro Tip: set -e has surprising exemptions — it won’t trigger inside if conditions, && chains, or commands followed by ||. Ask the AI “where will set -e NOT catch a failure in this script?” and it’ll point you at the exact lines that need explicit checking.

Add an ERR trap that tells you where it died

Strict mode exits, but a bare exit is useless at 2am. I ask the AI to add a trap that prints the failing line and command.

trap 'on_error $LINENO "$BASH_COMMAND"' ERR

on_error() {
  local line=$1 cmd=$2
  printf '[FATAL] line %s: command failed: %s\n' "$line" "$cmd" >&2
  exit 1
}

When the script blows up, you get [FATAL] line 87: command failed: tar -xzf "$artifact" instead of silence. The AI tends to over-engineer this with stack traces and color codes; I strip it back to what an on-call engineer actually needs.

Build the cleanup handler with EXIT

The half-extracted release directory was the scariest bug. The fix is an EXIT trap that runs whether the script succeeds or fails, cleaning up temp state idempotently.

WORKDIR="$(mktemp -d)"

cleanup() {
  local rc=$?
  rm -rf "$WORKDIR"
  if (( rc != 0 )); then
    printf '[WARN] aborted, temp dir removed\n' >&2
  fi
}
trap cleanup EXIT

Using mktemp -d means no hardcoded paths and no chance of rm -rf running against a real directory because a variable was empty. I asked the AI specifically: “rewrite this to use a temp directory so a failed run never leaves the release in a broken state.” It got it right, but I still verified mktemp was available on our minimal Alpine image — it wasn’t the GNU version, and the flags differed. The AI assumed GNU coreutils. Always check the AI’s platform assumptions.

Make the dangerous step reversible

The core of hardening is a back-out path. For a deploy that swaps a symlink, the safe pattern is to capture the current target before changing it.

deploy_release() {
  local new="$1"
  local link="/srv/app/current"
  local previous
  previous="$(readlink -f "$link" || true)"

  ln -sfn "$new" "$link"
  if ! health_check; then
    printf '[ERROR] health check failed, rolling back\n' >&2
    [[ -n "$previous" ]] && ln -sfn "$previous" "$link"
    return 1
  fi
}

The symlink swap is atomic, and we record previous so a failed health check rolls straight back. I prompted: “the deploy must be reversible — if the health check fails, restore the previous version.” The AI produced a workable version, but its first draft did a cp instead of a symlink swap, which is not atomic. I caught that in review. A junior would have made the same mistake; the difference is I knew to look.

Never let the AI near the real secrets

My original script sourced an env file with the database password. When I shared the script for hardening, I replaced every credential with a placeholder like __DB_PASSWORD__ first. The AI improved the secret handling — moving from a sourced file to reading from a secrets manager at runtime — without ever seeing a real value.

DB_PASSWORD="$(aws secretsmanager get-secret-value \
  --secret-id prod/app/db --query SecretString --output text)"

Treat the AI as an untrusted external contractor. It writes the plumbing; you connect the real water. For more on this boundary, see securing AI-generated bash scripts and our broader AI for Bash and Python Automation guides.

Review the diff, then test the failure paths

Hardening only counts if the failure paths actually work. I run the script with a deliberately broken artifact and confirm the trap fires, the temp dir is gone, and the symlink rolled back. The AI can even generate these fault-injection tests — pair this with our notes on writing idempotent automation scripts so re-running after a failure is always safe.

If you want a structured second pass, the code review dashboard runs a static scan plus a risk floor over shell scripts, and the prompt workspace keeps your hardening prompts reusable.

Conclusion

AI turned a dangerous 40-line script into a hardened one in an afternoon — but every safety property in it exists because a human reviewed, tested, and corrected the draft. Strict mode, traps, cleanup, and a back-out path are exactly the kind of boilerplate AI generates well and humans must verify ruthlessly. Use the speed, keep the judgment, and never hand it a real secret. Grab reusable hardening prompts from the prompts library or the prompt packs to make this a repeatable habit.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.