and Cleaning Them Up Reliably with mktemp and trap
A fixed path like /tmp/script.tmp in a shell script is an open invitation for race conditions, data leaks, and symlink attacks. mktemp guarantees unique, safe temporary files, and trap EXIT ensures they get cleaned up in every termination scenario.
Table of Contents
- 1. The problem with fixed /tmp paths
- 2. mktemp: safe temporary files and directories
- 3. trap EXIT: reliable cleanup registration
- 4. /tmp vs. /var/tmp: lifetime and choice
- 5. Understanding and preventing symlink attacks
- 6. Temporary directories and working environments
- 7. Temporary files in parallel processes
- 8. Cleanup strategies for complex scripts
- 9. Comparison: safe vs. unsafe temporary files
- 10. Summary
- 11. FAQ
1. The problem with fixed /tmp paths
Temporary files with fixed paths like /tmp/myscript.tmp in Bash scripts are a classic security problem. When two instances of the same script run at the same time, triggered by parallel cron jobs, manual runs, or distributed systems, they overwrite each other's temporary files. The result is a race condition that sometimes shows up as data loss, sometimes as a silent bug, and sometimes as a security hole.
The most dangerous thing about fixed /tmp paths for temporary files is predictability. An attacker who can run unprivileged processes on the same system knows the exact path the script will use. They can create a file or a symlink at that path before the script runs. If the script then runs with elevated privileges (as root) and writes the temporary file, it actually writes to the symlink's target, which could be a system file. This class of attack is called "TOCTOU" (Time of Check to Time of Use) and is well documented in CVE databases.
Failing to clean up temporary files is also a problem that gets underestimated. When a script exits on an error path due to set -e, a signal, or an explicit exit call without cleaning up, temporary files pile up in /tmp. On systems with little space or scripts that fail often, this can fill up /tmp and push the whole system into an unreadable state.
2. mktemp: safe temporary files and directories
mktemp solves the fixed-path security problem by atomically creating temporary files with a guaranteed unique name. The call tmpfile=$(mktemp) creates an empty file with a random, unique name like /tmp/tmp.Xz4K8m and prints the path to stdout. File creation is atomic: there is no window between checking for existence and creating the file for an attacker to exploit.
The template argument of mktemp allows descriptive names for temporary files: mktemp /tmp/deploy-XXXXXXXX creates a file like /tmp/deploy-Kf73nPqR. The X characters get replaced with random characters, at least three are required, six is standard. For temporary directories instead of files, use mktemp -d. The directory created gets permissions 700 by default, and files get 600, both readable only by the creating user.
#!/usr/bin/env bash
# safe-tmpfile.sh: Secure temporary file usage with mktemp and trap
set -euo pipefail
# Declare all temp resources at the top for visibility
TMPFILE=""
TMPDIR=""
cleanup() {
# Remove only if paths are set and exist, safe even if mktemp failed
[[ -n "$TMPFILE" ]] && rm -f "$TMPFILE"
[[ -n "$TMPDIR" ]] && rm -rf "$TMPDIR"
}
# Register cleanup BEFORE creating temp files, no orphan risk
trap cleanup EXIT
trap 'echo "[ABORT] Signal received"; exit 130' INT TERM HUP
# Create temp file and temp directory with descriptive templates
TMPFILE="$(mktemp /tmp/deploy-XXXXXXXX)"
TMPDIR="$(mktemp -d /tmp/deploy-work-XXXXXXXX)"
echo "[INFO] Using TMPFILE=$TMPFILE TMPDIR=$TMPDIR"
# Write data, file has mode 600 (owner-only) by default
echo "sensitive config data" > "$TMPFILE"
cp /etc/myapp/template.conf "$TMPDIR/config.conf"
# Use temp resources in a subshell, cleanup still fires on EXIT
(
cd "$TMPDIR"
process-config.sh config.conf > "$TMPFILE"
)
# Read result
cat "$TMPFILE"
# cleanup() fires automatically on script exit (normal or error)
A common mistake when handling temporary files: registering trap after the first mktemp call. If mktemp fails (full /tmp, missing permissions), no cleanup runs. The correct pattern is to initialize variables with an empty value, register trap immediately, and only then call mktemp. The cleanup function checks with [[ -n "$TMPFILE" ]] whether the variable is set before deleting anything.
3. trap EXIT: reliable cleanup registration
trap cleanup EXIT is the cornerstone of reliably handling temporary files. The EXIT trap runs in every termination scenario: normal script end, early exit via set -e, an explicit exit call, and, combined with signal traps, also on SIGINT (Ctrl+C) and SIGTERM. The EXIT trap itself does not fire on SIGKILL (9) or SIGHUP, these signals cannot be caught in Bash.
Multiple cleanup registrations for temporary files can be implemented with an array of cleanup actions. Instead of having one monolithic cleanup function that needs to know everything, each section of code registers its own cleanup action in a global array. The main cleanup function iterates the array in reverse and runs each action. This enables modular scripts where functions manage their own resources without polluting global variables.
#!/usr/bin/env bash
# modular-cleanup.sh: Stack-based cleanup for multiple temporary resources
set -euo pipefail
declare -a CLEANUP_STACK=()
# Push a cleanup action onto the stack
push_cleanup() {
CLEANUP_STACK+=("$*")
}
# Execute all cleanup actions in LIFO order (last in, first out)
run_cleanup() {
local exit_code=$?
for (( i=${#CLEANUP_STACK[@]}-1; i>=0; i-- )); do
eval "${CLEANUP_STACK[$i]}" || true
done
return $exit_code
}
trap run_cleanup EXIT
# Each section registers its own cleanup, no global knowledge needed
setup_database_dump() {
local dump_file
dump_file="$(mktemp /tmp/db-dump-XXXXXXXX.sql)"
push_cleanup "rm -f '$dump_file'"
echo "$dump_file" # return path via stdout
}
setup_work_dir() {
local work_dir
work_dir="$(mktemp -d /tmp/migration-XXXXXXXX)"
push_cleanup "rm -rf '$work_dir'"
echo "$work_dir"
}
DUMP_FILE="$(setup_database_dump)"
WORK_DIR="$(setup_work_dir)"
echo "Dump: $DUMP_FILE"
echo "Work: $WORK_DIR"
# Simulate work, cleanup fires in reverse order on any exit
mysqldump mydb > "$DUMP_FILE"
cp "$DUMP_FILE" "$WORK_DIR/"
4. /tmp vs. /var/tmp: lifetime and choice
Choosing between /tmp and /var/tmp for temporary files depends on the desired lifetime. /tmp is meant for short lived temporary files: systemd-tmpfiles typically clears /tmp on system boot, and many distributions mount /tmp as tmpfs directly in RAM, so files there take up no disk space but do use memory, and disappear on reboot. On systems with little RAM, a large tmpfs can exhaust memory.
/var/tmp survives reboots: systemd-tmpfiles only deletes files there after 30 days (configurable in /etc/tmpfiles.d/). It is suited for temporary files that need to persist between sessions or across multiple script runs, for example download caches, partial uploads, or checkpoint data. Using mktemp -p /var/tmp creates temporary files with the same safety as /tmp, but with a persistent lifetime.
5. Understanding and preventing symlink attacks
Symlink attacks against temporary files work like this: an attacker notices that a privileged script operates on a predictable path like /tmp/backup.tar. Before the script creates the file, the attacker creates a symlink /tmp/backup.tar -> /etc/passwd. The privileged script follows the symlink and overwrites /etc/passwd. Even if the script only reads, it can be redirected to leak sensitive data into a file the attacker controls.
mktemp prevents this attack through atomic creation: it checks for and creates the file in a single system call (open(O_CREAT|O_EXCL)) that fails if the file already exists. An attacker cannot exploit the window between check and creation because there is none. Additional protection comes from setting TMPDIR to a directory only accessible to the running user: export TMPDIR="$(mktemp -d ~/tmp.XXXXXXXX)". In a private TMPDIR, other users cannot plant symlinks.
#!/usr/bin/env bash
# private-tmpdir.sh: Use a private TMPDIR to eliminate symlink attack surface
set -euo pipefail
# Create a private temp directory under $HOME, no other users can write here
PRIVATE_TMPDIR="$(mktemp -d "${HOME}/tmp.XXXXXXXX")"
# Set TMPDIR so that all mktemp calls in this script (and subprocesses) use it
export TMPDIR="$PRIVATE_TMPDIR"
cleanup() {
rm -rf "$PRIVATE_TMPDIR"
}
trap cleanup EXIT
# All subsequent mktemp calls create files in the private directory
config_file="$(mktemp)" # e.g. ~/tmp.K3mPqR/tmp.Xf9kLm
staging_dir="$(mktemp -d)" # e.g. ~/tmp.K3mPqR/tmp.dir.8NqPx
echo "Private tmp: $PRIVATE_TMPDIR"
echo "Config: $config_file"
echo "Staging: $staging_dir"
# Verify: files are inside private dir (not world-writable /tmp)
if [[ "$config_file" != "${PRIVATE_TMPDIR}"/* ]]; then
echo "[ERROR] mktemp did not honor TMPDIR" >&2
exit 1
fi
# Sensitive operations without symlink risk
generate-config.sh > "$config_file"
deploy.sh --config "$config_file" --staging-dir "$staging_dir"
6. Temporary directories and working environments
Temporary directories created with mktemp -d are the safe alternative to mkdir /tmp/mydir. They have mode 700, so other users cannot read or write them. As a working directory for build processes, extraction operations, or multi-step pipelines that generate many intermediate files, temporary directories are preferable to a single temporary file: all intermediate files get removed automatically when the directory is deleted, without needing to register each individual file in the cleanup function.
The pattern cd "$TMPDIR" in a build script has a pitfall: if the script exits via set -e while the current directory is the temporary directory, and the cleanup function runs rm -rf "$TMPDIR", a subsequent cd can fail. The safer pattern is to use a subshell for the working directory change and never set the global cd to a temporary directory that will later be deleted.
7. Temporary files in parallel processes
In parallelized scripts that spawn multiple background processes, each process needs to manage its own temporary files. The correct pattern: each background process calls mktemp itself and registers its own cleanup via trap in the subshell. The main process has no access to the temporary files of the child processes and should not try to manage them centrally. As soon as a child process terminates, whether normally or by the main process, its EXIT trap fires and cleans up.
An alternative for temporary files in parallel processes: all processes work in subdirectories of a shared temporary directory managed by the main process. Each child process gets its own subdirectory path passed in: workdir=$(mktemp -d "${SHARED_TMPDIR}/worker-XXXXXXXX"). The main process cleans up the entire temporary directory after wait. This simplifies cleanup at the cost of isolation: if a child process dies without cleaning up, the main process still has access to its data.
8. Cleanup strategies for complex scripts
In complex scripts that create many temporary files, manually tracking paths in individual variables becomes unwieldy. The stack-based cleanup pattern from section 3 is one solution. Another is directory-based cleanup: all of the script's temporary files get created within a single mktemp -d directory. The cleanup function only needs to delete that directory with rm -rf, regardless of how many files ended up inside. No tracking, no list, nothing to forget.
For long-running scripts or daemon-like processes that hold temporary files for hours, an explicit inventory directory is worth considering: all active temporary files get registered in an index file (itself a safe temporary file). A periodic cleanup run checks whether the associated processes are still alive and deletes orphaned temporary files from terminated processes. This prevents accumulation over long runtimes without needing full control over every cleanup path.
9. Comparison: safe vs. unsafe temporary files
The table below shows the most common patterns for temporary files in Bash scripts and their security and robustness characteristics.
| Pattern | Race safe | Symlink safe | Cleanup |
|---|---|---|---|
/tmp/script.tmp |
No | No | Manual, often forgotten |
/tmp/script-$$.tmp |
Partial | No | Only if a trap exists |
mktemp |
Yes (O_EXCL) | Yes | Only with trap EXIT |
mktemp + trap EXIT |
Yes | Yes | All scenarios |
mktemp + private TMPDIR |
Yes | Maximum | All scenarios |
Using the PID ($$) in the filename only protects against concurrent runs with different PIDs. It does not protect against symlink attacks, because the PID of the next script is predictable: /proc/sys/kernel/pid_max caps PIDs, and on a quiet system they follow predictable patterns. Only mktemp with cryptographically random suffixes is truly safe. The full pattern of mktemp + trap EXIT + private TMPDIR is the only solution that covers every threat class for temporary files at once.
Mironsoft
Shell security, Bash auditing, and DevOps infrastructure
Need your shell scripts checked for security holes?
We analyze existing Bash scripts for unsafe temporary files, missing trap registrations, and symlink vulnerabilities, then fix them with the full mktemp plus trap pattern.
Security audit
Analyze scripts for TOCTOU issues, symlink attacks, and predictable paths
Refactoring
Replace fixed /tmp paths with mktemp plus trap EXIT plus private TMPDIR
ShellCheck CI
Automated checks in your pipeline so unsafe patterns never reach production
10. Summary
Using temporary files safely in Bash requires three components: mktemp for atomic, secure creation with a random name; trap cleanup EXIT for reliable cleanup in every termination scenario; and optionally a private TMPDIR for maximum protection against symlink attacks. Fixed paths like /tmp/script.tmp are not acceptable in any production script: they are race prone, symlink vulnerable, and often never cleaned up.
The cleanup function must be registered before the first mktemp call and must never ignore the error case. Stack-based cleanup patterns enable modular scripts where each function manages its own temporary files. Directory-based cleanup (mktemp -d plus rm -rf) is more elegant than tracking individual paths when many intermediate files are involved. ShellCheck statically catches typical mistakes with temporary files and should be part of every CI pipeline.
Using temporary files safely: the essentials at a glance
Always mktemp
Never use fixed /tmp paths. mktemp creates atomically, uniquely, and with permissions 600/700, the baseline requirement for safe temporary files.
trap before mktemp
trap cleanup EXIT must come before the first mktemp call. Cleanup checks with [[ -n "$TMPFILE" ]] before deleting.
/tmp vs. /var/tmp
/tmp: short lived, often tmpfs in RAM, cleared on reboot. /var/tmp: survives reboots, cleaned only after 30 days.
Private TMPDIR
export TMPDIR="$(mktemp -d ~/tmp.XXXXXXXX)" eliminates the symlink attack surface entirely, no other user can write to the directory.