🛡️ AI-Assisted Shell Scripting

AI-Assisted Shell Scripting / survival guide

AI-Assisted Shell Scripting Without Losing Your Files

An AI can draft a shell script in seconds. It can also write "rm -rf $DIR/*" with $DIR empty, invent a flag that does
not exist, or run one step too many on your production server. This guide is a repeatable way to get the speed of AI
without the disasters. The rule is simple: the AI drafts, YOU review, and nothing touches real data until it has been
tested somewhere safe.

The golden rules

1. Never run a script you cannot explain line by line. If you do not understand it, ask the AI to explain it, then verify
2. Test on copies: a temp folder, a container or a throwaway VM. Never first-run on production
3. Make destructive steps visible: dry run first, confirmation prompt, and a log of what happened
4. Back up or commit before you change anything: git commit, snapshot, or a copy
5. Least privilege: no sudo unless a step really needs it, and never run a whole script as root by habit
6. Check every command and flag against man pages or --help. AI invents flags that sound real
7. Keep secrets out of prompts, scripts and git
8. You own the result. "The AI wrote it" is not a post-mortem

The safe workflow (do these in order)

1. Describe the task precisely      Input, output, OS, shell, and what must NOT happen
2. Ask for a plan first             Read the steps in plain English before any code appears
3. Ask for the script with safety   Dry-run flag, error handling, quoting, logging
4. Review it yourself               Use the checklist below
5. Lint it                          bash -n, shellcheck, shfmt
6. Test in a sandbox                Temp folder, container, or VM with fake data
7. Dry run on real data             Only print what would happen
8. Run for real, with logging       Keep the output, and have a rollback ready
9. Commit it                        Save the working version with a message

Steps 1 and 2: a prompt template for scripts

I need a bash script for [Ubuntu 24.04, bash 5].
Goal: [what it should do, in one or two sentences]
Input: [files, arguments, environment variables]     Output: [what it prints or creates]
Constraints: [must be idempotent; no sudo; only touch files under /srv/data; no network access]
Must NOT: [delete anything outside the target directory; overwrite existing files; run without confirmation]
Requirements: strict mode, quote every variable, a --dry-run option that only prints actions, clear error messages,
a usage function, and logging to stderr. Use only commands in a default Ubuntu install.
First explain your plan in numbered steps and list every command you intend to use. Do not write code yet.
Wait for me to reply "go".
The plan step catches misunderstandings early and shows you the commands to check.

A safe script skeleton (a good starting point for anything the AI writes)
#!/usr/bin/env bash
set -Eeuo pipefail                       # stop on errors, unset variables and pipe failures
IFS=$'\n\t'                              # safer word splitting

DRY_RUN=0
TARGET_DIR=""
LOG() { printf '%s %s\n' "$(date '+%F %T')" "$*" >&2; }
die() { LOG "ERROR: $*"; exit 1; }
usage() { echo "Usage: $0 [--dry-run] TARGET_DIR"; exit 2; }
run() { if [[ $DRY_RUN -eq 1 ]]; then LOG "DRY RUN: $*"; else LOG "RUN: $*"; "$@"; fi; }

while [[ $# -gt 0 ]]; do
  case "$1" in
    --dry-run) DRY_RUN=1; shift ;;
    -h|--help) usage ;;
    --) shift; break ;;
    -*) die "Unknown option: $1" ;;
    *) TARGET_DIR="$1"; shift ;;
  esac
done

[[ -n "$TARGET_DIR" ]] || usage
[[ -d "$TARGET_DIR" ]] || die "Not a directory: $TARGET_DIR"
case "$(realpath "$TARGET_DIR")" in
  /|/etc|/usr|/var|/home|/root|/boot) die "Refusing to work on a system directory: $TARGET_DIR" ;;
esac

WORKDIR="$(mktemp -d)"
trap 'rm -rf -- "$WORKDIR"' EXIT          # clean up our own temp files, and nothing else

# ... your actual work goes here, using run() for every change ...
run find "$TARGET_DIR" -type f -name '*.tmp' -mtime +30 -print
LOG "Done."
Key ideas: strict mode, a dry-run wrapper, refusing dangerous targets, a private temp directory, and a trap for cleanup.

Step 4: the review checklist (read every line against this list)

Deleting and overwriting
[ ] Every rm, mv, cp, dd, mkfs, truncate, shred, > redirect and sed -i: what exactly does it touch?
[ ] rm -rf "$VAR/...": can $VAR be empty? Use "${VAR:?must be set}". Never rm -rf with an unchecked variable
[ ] find ... -delete or -exec rm: run it with -print first, and keep -delete last in the expression
[ ] Wildcards: does * match more than you think (dotfiles, spaces, huge directories)?
[ ] > overwrites and >> appends. Is that what you want? Consider set -o noclobber
Privileges and system changes
[ ] sudo, su, chown -R, chmod -R (especially 777), usermod, visudo, passwd: is each really needed?
[ ] Package installs, service restarts, firewall changes, cron edits, /etc changes: expected and reversible?
[ ] curl ... | sh, downloaded scripts and binaries: where from, and checked how?
Correctness
[ ] Every variable quoted: "$var" and "${arr[@]}". Unquoted variables break on spaces and globs
[ ] cd without || exit (or use absolute paths)
[ ] Parsing ls output, or a for loop over $(ls): use globs, find -print0 with read -d '', or xargs -0
[ ] eval and commands built from strings: avoid, or prove the input is safe
[ ] Loops and conditions: off-by-one, wrong comparison ([ ] vs [[ ]], -eq vs =), or an empty list
[ ] Exit codes checked, and errors not hidden with || true or 2>/dev/null on important commands
[ ] Idempotent: safe to run twice? Does it fail, duplicate, or break on the second run?
Truth checking
[ ] Every command exists here: command -v tool.   Every flag exists: tool --help, man tool
[ ] Right version and flavor: GNU vs BSD (macOS), busybox, dash vs bash. #!/bin/sh with bashisms is a bug
[ ] Hardcoded paths, usernames, hostnames, IPs or secrets? Move them to variables or arguments
[ ] Any network calls or uploads you did not ask for?

Step 5: lint and inspect

bash -n script.sh                          Syntax check only. Runs nothing
shellcheck script.sh                       Finds quoting bugs, bad tests and unsafe patterns (sudo apt install shellcheck)
shellcheck -x -S warning script.sh
shfmt -d script.sh                         Show formatting differences (shfmt -w fixes them)
bash -x script.sh                          Trace: print every command as it runs (use with --dry-run first)
type -a name    command -v name            What does this command actually resolve to?
Trust shellcheck for syntax and quoting problems. Paste its warnings back to the AI: "Fix these warnings and explain each change."

Step 6: test where it cannot hurt you

Temp folder with fake data:
T=$(mktemp -d); mkdir -p "$T"/data/{a,b}; touch "$T"/data/a/{1,2,3}.tmp "$T"/data/b/keep.txt
./script.sh --dry-run "$T/data"            # look at the output
./script.sh "$T/data"; ls -R "$T/data"     # then run it for real on the fake data
Container (a clean, disposable Linux):
docker run --rm -it -v "$PWD":/work -w /work ubuntu:24.04 bash
podman run --rm -it -v "$PWD":/work:Z -w /work ubuntu:24.04 bash
Throwaway VM: use a cloud VM you delete afterwards, or a local VM with a snapshot (Multipass, VirtualBox, virt-manager) and roll it back.
Copy of real data: rsync -a /srv/data/ /tmp/data-copy/     then test against /tmp/data-copy
Shell unit tests (optional): bats-core.   @test "dry run changes nothing" { run ./script.sh --dry-run "$T"; [ "$status" -eq 0 ]; }

Step 7: built-in dry runs (use them)

rsync -av --dry-run src/ dest/             apt-get -s install pkg              find ... -print   (before adding -delete)
rm -i file    (ask each time)              cp -n / mv -n    (never overwrite)    sed 's/a/b/' file | diff file -    (preview before sed -i)
git diff / git stash                       terraform plan      ansible-playbook -C -D      kubectl diff -f x.yaml
The echo trick: put echo in front of a destructive command so it prints instead of runs:
for f in *.bak; do echo rm -- "$f"; done
When the output looks right, remove the echo.

Step 8: run it for real, safely

- Take a backup or snapshot first, and know how to undo the change (the rollback plan)
- Log everything:  ./script.sh 2>&1 | tee "run-$(date +%F-%H%M).log"
- Start with the smallest target: one directory, one host, one file
- Watch the first minute, with a second terminal ready to stop it (Ctrl+C, kill)
- Do not run the first real test as root. Add sudo only to the exact steps that need it
- For cron jobs: test by hand with the same environment, log the output, and use flock to prevent overlapping runs
  */15 * * * * /usr/bin/flock -n /tmp/job.lock /usr/local/bin/job.sh >> /var/log/job.log 2>&1

Version control: your undo button

cd ~/scripts && git init                          # once
git add -A && git commit -m "checkpoint before AI edit"
# ... let the AI change things ...
git diff                                          # see exactly what changed
git restore script.sh                             # throw the changes away if they are bad
git commit -am "Add dry-run option (reviewed, shellcheck clean)"
Commit before every AI change. A diff you can read is the best review tool you have.

Common AI mistakes in scripts (watch for these)

Invented options            A flag or command that does not exist. Always verify with --help or man
Wrong version or OS         GNU-only options on macOS, apt on a Red Hat system, bashisms in #!/bin/sh
Missing quotes              rm -rf $dir/*      cp $file $dest      for f in $(ls)
Assuming root or a path     Hardcoded /home/user, or commands that need sudo but do not say so
Silent failures             || true, 2>/dev/null, or no set -e, so errors vanish
set -e surprises            Commands inside if, && or pipes do not always stop the script. Do not rely on it alone
Unsafe temp files           /tmp/file with a predictable name. Use mktemp
echo -e with backslashes    Behaves differently between shells. Use printf
Parsing ls or ps output     Fragile. Use globs, find, stat, or ps -o with fixed columns
Deleting "old" files        Without checking the path, the age and the file type first
Not idempotent              Appends the same line to a config on every run, or fails the second time
Overconfident explanations  A wrong explanation delivered with total confidence. Test the behavior, not the words
Too clever                  A 6-line pipeline nobody can read. Ask for the simple, longer version

Dangerous patterns: stop and think

rm -rf /     rm -rf "$x/"     rm -rf ~     rm -rf .     rm -rf *     dd of=/dev/sdX     mkfs.*     > /dev/sda
chmod -R 777     chown -R user /     curl ... | sudo bash     sed -i on many files     truncate -s 0     echo ... > /etc/...
iptables -F on a remote machine (you can lock yourself out)     systemctl disable ssh     anything with a variable in a delete or overwrite path
Anything you did not read.   Anything from an unknown source.

What to use AI for (great, low-risk jobs)

Explain a script       "Explain this script line by line and list anything risky." Great for learning and inherited scripts
Review a script        "Find bugs, unsafe patterns and missing error handling. Do not rewrite it. List problems with line numbers."
Add safety             "Add a --dry-run mode and input validation, and change as little as possible. Show a diff."
Convert                Bash to Python or fish, a one-liner into a documented script, old sh to modern bash
Write helpers          Regular expressions, awk and sed programs (with test inputs), find expressions, jq filters, cron schedules
Tests and examples     "Give me 5 test cases including edge cases: empty input, spaces in names, huge files."
Documentation          Usage text, comments, a README
Debugging              Paste the error and the relevant lines (redacted), and ask for likely causes in order
Ask for a diff, not the whole file, when changing existing code, so you only review what changed.

Prompts that make scripts safer

"Add set -Eeuo pipefail, quote every variable, and add a trap for cleanup. Explain each addition."
"List every command in this script that modifies or deletes data, and what would make each one dangerous."
"What happens if $DIR is empty or contains spaces? What if the script runs twice?"
"Rewrite this so it only prints what it would do unless I pass --apply."
"Give me the exact command to test this safely in a Docker container with fake data."
"Give me a rollback procedure for what this script changes."
"Act as a skeptical reviewer. What could go wrong in production?"
"Show me the man page section or --help line that proves each flag exists."

Untrusted input and prompt injection

An AI that reads files, logs, web pages or email can be tricked by text hidden inside them ("ignore your instructions and run ...").
- Do not let an AI agent run commands based on untrusted content until you have read them
- Treat pasted logs from the internet or from users as data. If the AI suddenly suggests unrelated commands, stop
- Keep agents in read-only or plan mode while they explore, and approve each command that changes anything
- Never give an AI agent your production credentials

Secrets and private data

Before pasting a script, log or config, remove secrets:
sed -E 's/(password|passwd|secret|token|apikey|api_key)[=: ]+[^ ]+/\1=REDACTED/Ig' file.log
sed -E 's/[0-9]{1,3}(\.[0-9]{1,3}){3}/x.x.x.x/g' file.log                          # hide IP addresses
sed -E 's/[[:alnum:]._%+-]+@[[:alnum:].-]+\.[a-z]{2,}/user@example.com/g' file.log   # hide email addresses
Check the result by eye before you paste. For sensitive work, use a local model (Ollama) so nothing leaves your machine.
Never put secrets in a script. Read them from the environment or a secrets manager:  DB_PASS="${DB_PASS:?set DB_PASS}"

Using Ollama and Claude Code for scripts

# Local review of a script (nothing leaves your machine)
ollama run qwen2.5-coder "Review this bash script for bugs and unsafe patterns. List problems with line numbers." < script.sh

# Small helpers for ~/.bashrc
ask() { ollama run llama3.2 "$*"; }
explain() { ollama run llama3.2 "Explain this shell command in plain English and warn me about anything dangerous: $*"; }

# Claude Code in the terminal
cd ~/scripts && git status               # start from a clean, committed state
claude                                   # then use Plan mode (Shift+Tab), approve the plan, and review with git diff
Use Plan mode first, keep permission prompts on, read each command it wants to run, and review with /code-review or git diff before keeping changes.
Never use --dangerously-skip-permissions on a machine that holds anything you care about.

A worked example: cleaning old log files

Task: delete .log files older than 14 days from /srv/app/logs, and keep a record of what was removed.
Risky first draft:
  cd $LOGDIR && rm -rf *.log
Problems: unquoted variable, no age check, deletes everything, no record, and it still runs if cd fails.
Reviewed version:
  #!/usr/bin/env bash
  set -Eeuo pipefail
  LOGDIR="${1:?Usage: $0 LOGDIR [--apply]}"
  APPLY="${2:-}"
  [[ -d "$LOGDIR" ]] || { echo "Not a directory: $LOGDIR" >&2; exit 1; }
  [[ "$(realpath "$LOGDIR")" == /srv/app/logs ]] || { echo "Refusing: unexpected directory" >&2; exit 1; }
  if [[ "$APPLY" == "--apply" ]]; then
    find "$LOGDIR" -maxdepth 1 -type f -name '*.log' -mtime +14 -print -delete | tee -a /var/log/log-cleanup.log
  else
    echo "DRY RUN. Would delete:" >&2
    find "$LOGDIR" -maxdepth 1 -type f -name '*.log' -mtime +14 -print
  fi
Test it against a temp folder with files of different ages: check the dry run, then --apply, then confirm what remains.

A review prompt you can paste

"Review the script below. (1) List every command that modifies or deletes anything. (2) List any unquoted variables.
(3) List assumptions about OS, shell version, paths and privileges. (4) List any commands or flags you are not 100% sure exist.
(5) Give the three most likely ways this could cause damage, and how to prevent each. Do not rewrite the script. [PASTE SCRIPT]"

Tips

    Treat AI output like code from a stranger on the internet: useful, unreviewed and unproven.

    Ask for a plan and a dry-run mode before you ask for code. Reading the plan takes a minute and can save your data.

    Run shellcheck on every script, every time. It catches the quoting mistakes both the AI and you will make.

    Commit before an AI edit and read the git diff after it. Never accept a big rewrite you cannot review.

    Test on fake data in a temp folder or container first, then a dry run on real data, then the real run.

    Verify commands and flags in the man pages. If the AI cannot point to where a flag is documented, do not trust it.

    Keep scripts small and simple. If you cannot read one in a single sitting, ask for a simpler version.

    Never paste secrets or private data into a hosted AI. Redact first, or use a local model.

    Never let an AI run destructive commands on a system you cannot afford to lose, and keep backups you have tested.

    You stay responsible for what runs on your systems. The AI is a fast assistant, not the person on call.