Bash trap Cleanup and Temp File Management for Safe Scripts
Stop leaving stale temp files and half-finished state behind. Use Bash trap and mktemp to build automation that cleans up after itself, even when it crashes.
- #bash
- #python
- #reliability
The first time a backup script of mine died halfway through, it left a 40 GB partial tarball sitting in /tmp and a lock file that blocked every future run. Nobody noticed until the disk filled up at 3 a.m. The fix was not a bigger disk. It was teaching the script to clean up after itself no matter how it exited.
That is what trap is for, and it is one of the most under-used tools in Bash. In this guide I will show you how I wire up cleanup that fires on success, failure, and Ctrl-C alike, and how I let AI draft the boilerplate while I keep my hands on the part that actually deletes files.
Why naive cleanup fails
The obvious approach is to delete the temp file at the bottom of the script:
tmpfile=$(mktemp)
do_work > "$tmpfile"
process "$tmpfile"
rm "$tmpfile" # never reached if process() exits non-zero
If do_work or process fails, or someone hits Ctrl-C, that rm never runs. The temp file leaks. Multiply that across thousands of cron invocations and you have a slow-motion outage.
trap solves this by registering a command that the shell runs when it receives a given signal or pseudo-signal. The one you want most is EXIT, which fires on any exit path.
A cleanup trap that always runs
#!/usr/bin/env bash
set -euo pipefail
tmpdir=$(mktemp -d)
cleanup() {
rm -rf "$tmpdir"
}
trap cleanup EXIT
# Everything below can use $tmpdir freely.
curl -fsSL https://example.com/data.json -o "$tmpdir/data.json"
jq '.items[]' "$tmpdir/data.json" > "$tmpdir/items.txt"
process "$tmpdir/items.txt"
Because the trap is on EXIT, cleanup runs whether the script finishes normally, hits set -e on an error, or gets killed by most signals. You write the cleanup logic once and forget about it.
Pro Tip: Register the trap immediately after you create the resource, not at the end. If the trap is set on line 50 but mktemp ran on line 5, a crash on line 30 leaks the file. Create, then trap, then use.
Handling signals explicitly
EXIT does not fire for SIGKILL (nothing can catch that) but it does cover normal termination and SIGINT/SIGTERM because those cause the shell to exit. If you want to do something different on an interrupt versus a clean exit, trap them separately:
cleanup() {
rm -rf "$tmpdir"
}
on_interrupt() {
echo "Interrupted, rolling back partial changes..." >&2
rollback
exit 130 # 128 + SIGINT(2)
}
trap cleanup EXIT
trap on_interrupt INT TERM
The EXIT trap still runs after on_interrupt calls exit, so the temp dir gets cleaned regardless. Layering traps this way keeps each handler small and single-purpose.
Use mktemp, never a hard-coded path
I see /tmp/mybackup.tar in scripts all the time. It is a race condition and a security hole: a malicious local user can pre-create that path as a symlink and redirect your write. mktemp creates a guaranteed-unique file or directory with safe permissions:
# A unique file
tmpfile=$(mktemp)
# A unique directory
tmpdir=$(mktemp -d)
# With a recognizable prefix so it's obvious in `ls /tmp`
tmpfile=$(mktemp -t myscript.XXXXXX)
The XXXXXX becomes random characters. Always quote the result, since paths can theoretically contain spaces.
Capturing the exit code inside the trap
Sometimes cleanup should behave differently depending on whether the script succeeded. The trick is that inside an EXIT trap, $? holds the exit status of the last command before exit:
cleanup() {
local rc=$?
rm -rf "$tmpdir"
if (( rc != 0 )); then
echo "Script failed with code $rc; check $logfile" >&2
fi
exit "$rc"
}
trap cleanup EXIT
Grab $? on the very first line of the trap before any other command overwrites it. This pattern lets you send an alert on failure without scattering error handling through the whole script.
Letting AI write the boilerplate
trap syntax is fiddly and easy to get subtly wrong, which makes it perfect work for an AI assistant acting as a fast junior engineer. I will paste a rough script into Claude or Cursor and ask: “Add an EXIT trap that removes any temp files, preserves the original exit code, and logs a warning on non-zero exit.”
It produces clean boilerplate in seconds. But I read every line before it touches a real machine, because the failure mode here is destructive. A generated trap that runs rm -rf "$tmpdir/" where $tmpdir could be empty becomes rm -rf /. I check that:
- Every variable in an
rmpath is set and non-empty (${tmpdir:?}guards this). - The trap does not accidentally exit before cleanup finishes.
- No real secrets were pasted into the prompt to “give context.” The AI never needs your actual credentials, and you should never hand them over.
For a structured way to keep these review habits, I keep a checklist in the prompt workspace and run risky scripts through the code review dashboard before they reach production.
Guarding against empty variables
This is the single highest-value safety check, so it gets its own section. Combine set -u with parameter-expansion guards:
set -euo pipefail
tmpdir=$(mktemp -d) || exit 1
cleanup() {
# ${tmpdir:?} aborts if tmpdir is unset or empty
rm -rf "${tmpdir:?temp dir not set}"
}
trap cleanup EXIT
If tmpdir is somehow empty, ${tmpdir:?...} errors out instead of deleting your whole filesystem. I treat this as non-negotiable in any script that does recursive deletes.
The Python equivalent
If your automation is in Python, the standard library already does the right thing with context managers, which I reach for instead of writing manual cleanup:
import tempfile
import shutil
from pathlib import Path
with tempfile.TemporaryDirectory() as td:
tmp = Path(td)
(tmp / "data.json").write_text(fetch_data())
process(tmp / "data.json")
# Directory is removed automatically, even on exception.
The with block is Python’s trap EXIT: the directory is deleted when the block ends, including when an exception unwinds it. For single files, tempfile.NamedTemporaryFile works the same way. Reach for these before rolling your own.
Conclusion
Cleanup is not glamorous, but it is the difference between automation you can trust unattended and a script that fills disks at 3 a.m. Create the resource, register a trap cleanup EXIT immediately, guard your delete paths against empty variables, and let an AI assistant draft the boilerplate while you review every destructive line. The shell will handle the rest, even when your script does not finish the way you hoped.
Want more patterns like this? Browse the full Bash and Python automation category, grab reusable starters from the prompt library, or pick up a curated set from the prompt packs.
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.