Linux Basic — Safe Temporary Files
Safe Temporary Files
Almost every non-trivial script needs a scratch file or directory — to build a payload, hold an intermediate result, or work around a tool that won’t read stdin. Doing this safely takes about three lines. Doing it unsafely opens a privilege-escalation vulnerability that has bitten famous projects more than once.
The Bad Old Way — /tmp/myscript.$$
For decades, scripts created temp files like this:
# ❌ Insecure
tmp="/tmp/myscript.$$"
echo "data" > "$tmp"
process "$tmp"
rm "$tmp"
Two problems:
- Predictable name.
$$is the script’s PID — public information. An attacker can pre-create/tmp/myscript.NNNas a symlink to/etc/passwdand now your script will overwrite/etc/passwd. - Race condition. Between the test “does the file exist?” and the create, an attacker can win the race.
Don’t write this. Ever.
The Modern Way — mktemp
mktemp(1) atomically creates a uniquely-named file (or directory) with safe permissions and prints its path:
tmp=$(mktemp)
echo "$tmp"
# /tmp/tmp.AbCdEf1234
ls -l "$tmp"
# -rw------- 1 ubuntu ubuntu 0 Jun 1 12:00 /tmp/tmp.AbCdEf1234
Note the permissions: rw------- (mode 0600) — only you can read or write. mktemp guarantees:
- A unique filename (collision-free)
- Atomic create (no race window)
- Restrictive permissions
Templates
You can specify a name template so logs are easier to identify. The template needs at least three trailing Xs, which mktemp replaces with random characters:
tmp=$(mktemp /tmp/myscript.XXXXXX)
# /tmp/myscript.f7Hk2L
# Use the system tmp dir but give it a meaningful name
tmp=$(mktemp -t mybuild.XXXXXX)
# /tmp/mybuild.r3Tx8q
Temp directories
For multiple related files, make one tempdir and put them all in it:
tmpdir=$(mktemp -d -t mybuild.XXXXXX)
echo "$tmpdir"
# /tmp/mybuild.aQ7Ymb
touch "$tmpdir/in.txt" "$tmpdir/out.txt" "$tmpdir/log.txt"
ls "$tmpdir"
# in.txt log.txt out.txt
Always Clean Up With trap
Creating a temp file is half the job. Removing it on every exit path — success, error, Ctrl+C, killed by a signal — is the other half. The trap builtin runs a command when the shell exits:
tmp=$(mktemp)
trap 'rm -f -- "$tmp"' EXIT
EXIT fires for every exit: normal end, exit 1, set -e killing the script, even an unhandled signal. That single line guarantees cleanup.
For a tempdir:
tmpdir=$(mktemp -d)
trap 'rm -rf -- "$tmpdir"' EXIT
A fuller template you can paste at the top of every script:
#!/usr/bin/env bash
set -euo pipefail
tmpdir=$(mktemp -d -t myscript.XXXXXX)
trap 'rm -rf -- "$tmpdir"' EXIT
# ... use $tmpdir freely ...
echo "data" > "$tmpdir/payload"
process "$tmpdir/payload"
When the script ends — for any reason — the tempdir disappears.
What If You Need Multiple Cleanup Actions?
trap is “last writer wins” by default — re-registering a handler replaces the previous one. Use a cleanup function:
cleanup() {
rm -rf -- "$tmpdir"
[ -n "${tmpfile:-}" ] && rm -f -- "$tmpfile"
kill "$bg_pid" 2>/dev/null || true
}
trap cleanup EXIT
Now everything that needs to happen on exit lives in one function.
TMPDIR — Respect the Environment
mktemp chooses where to put the file in this order:
- The directory you give it (e.g.
mktemp /var/cache/foo.XXXX) $TMPDIRif set/tmpas a fallback
Letting the system / user override TMPDIR is good citizenship:
- CI runners often set
$TMPDIRto a per-job scratch area - macOS uses a per-user
$TMPDIR(tryecho $TMPDIR) - Containers may have a tmpfs only at a non-
/tmppath
TMPDIR="/var/tmp" mktemp -t myscript.XXXXXX
# /var/tmp/myscript.x9P3Lf
Never hard-code /tmp unless you really must.
Permissions and Sensitive Data
mktemp defaults to 0600 — readable only by you. For most cases that’s fine. If you need it tighter:
tmp=$(mktemp)
chmod 0400 "$tmp" # read-only, even for owner
If the file holds secrets, also consider:
- Putting it on
tmpfs(in-memory;/dev/shmor atmpfs-mounted/tmp) - Shredding it before
rm:shred -u "$tmp"(rotational disks; SSDs need different treatment) - Setting a tighter
umaskat the top of the script:umask 077
Cross-Platform Notes
| Tool | GNU (Linux) | BSD (macOS) |
|---|---|---|
mktemp |
takes a template anywhere | template must end with Xs |
mktemp -p DIR |
works | ❌ — use mktemp DIR/foo.XXXX |
mktemp -t prefix |
uses $TMPDIR (/tmp/prefix.XXX) |
uses $TMPDIR (/var/folders/...) |
mktemp -d |
works | works |
For cross-platform scripts, the most portable form is:
tmp=$(mktemp "${TMPDIR:-/tmp}/myscript.XXXXXX")
Putting It All Together
A safe, copy-paste-ready template:
#!/usr/bin/env bash
set -euo pipefail
# 1. Create a private workspace
work=$(mktemp -d -t myjob.XXXXXX)
trap 'rm -rf -- "$work"' EXIT
# 2. Use it
curl -sSf https://example.com/data.json -o "$work/data.json"
jq '.items[] | .id' "$work/data.json" > "$work/ids.txt"
# 3. Produce final output atomically
out="/var/lib/myjob/ids.txt"
mv -- "$work/ids.txt" "$out"
# trap fires here — tempdir vanishes
That eight-line skeleton handles uniqueness, privacy, cleanup, and atomic delivery.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
tmp="/tmp/$$.txt" |
tmp=$(mktemp) |
predictable name = symlink attack |
mktemp without trap |
tmp=$(mktemp); trap 'rm -f -- "$tmp"' EXIT |
leaks on failure |
rm $tmp (unquoted) |
rm -- "$tmp" |
spaces/special chars + leading-dash issues |
| Reuse same name across runs | unique name per run via mktemp |
concurrent runs collide |
mktemp /var/foo.XXX then mv $tmp /var/foo.json across FS |
keep tempfile and target on same filesystem | cross-FS mv isn’t atomic |
Next → Practice Exercises