Safe Temporary Files and Lock Directories
Use mktemp and flock to create race-free temp resources and prevent concurrent script runs.
Safe Temporary Files and Lock Directories is a free DevOps Bootcamp lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Temporary Files Are a Security Risk
Bash scripts frequently need temporary storage — intermediate results, lock markers, staging areas. But creating temp files carelessly opens serious vulnerabilities.
- Race conditions: Another process can predict your filename and create the file first, redirecting your writes.
- Symlink attacks: An attacker creates a symlink at your expected path pointing to a sensitive file like
/etc/passwd. - Leftover files: If a script crashes, temp files pile up and may expose sensitive data.
The two core tools that eliminate these problems are mktemp and flock. This lesson shows you how to use both safely and defensively.
Creating Safe Temp Files with mktemp
mktemp creates a temporary file with a random, unpredictable name and returns its path. It atomically creates the file, so no other process can grab the name first.
- Syntax:
mktemp [TEMPLATE]— the template must end in at least threeXcharacters. - Each
Xis replaced by a random character, producing a unique name like/tmp/script.aB3kQz. - The file is created with permissions
0600(readable only by the owner) automatically.
Always capture the returned path into a variable immediately so you can reference and clean it up later.
#!/usr/bin/env bash
set -euo pipefail
# Create a secure temp file
TMPFILE=$(mktemp /tmp/myapp.XXXXXX)
echo "Temp file created at: $TMPFILE"
# Write data to it
echo "some intermediate result" > "$TMPFILE"
# Read it back
cat "$TMPFILE"
# Clean up
rm -f "$TMPFILE"Always Clean Up with a trap
If your script exits unexpectedly — due to an error, a signal, or set -e triggering — temp files will be left behind unless you register a cleanup handler.
The trap built-in runs a command when the shell receives a signal or exits. The canonical pattern for temp file cleanup is:
- Register the trap immediately after creating the temp file.
- Trap on
EXITso cleanup runs on both normal and abnormal exit. - Also trap
INTandTERMif the script runs long or is interactive.
This guarantees no orphan files, even if the script is killed mid-execution.
#!/usr/bin/env bash
set -euo pipefail
TMPFILE=$(mktemp /tmp/report.XXXXXX)
# Register cleanup before doing any real work
cleanup() {
rm -f "$TMPFILE"
echo "Cleaned up $TMPFILE" >&2
}
trap cleanup EXIT
# Do work — even if this fails, cleanup() will run
echo "Processing..." > "$TMPFILE"
grep "result" "$TMPFILE" || true
echo "Done. File will be removed on exit."Creating Temp Directories with mktemp -d
Sometimes you need an entire directory for staging multiple files — for example, building an archive or extracting a tarball before processing. Use mktemp -d to create a secure temporary directory.
- The directory is created with permissions
0700(owner-only access). - Clean up with
rm -rfin your trap — be careful to only remove the variable, never a hardcoded path. - Use double-quoting and verify the variable is non-empty before calling
rm -rfas an extra safety check.
#!/usr/bin/env bash
set -euo pipefail
TMPDIR=$(mktemp -d /tmp/extract.XXXXXX)
cleanup() {
# Guard: only rm if variable is set and non-empty
[[ -n "${TMPDIR:-}" ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT
echo "Working in $TMPDIR"
# Simulate staging files
echo "file one" > "$TMPDIR/part1.txt"
echo "file two" > "$TMPDIR/part2.txt"
ls "$TMPDIR"
echo "All done."The Problem of Concurrent Script Runs
Cron jobs, systemd timers, and manually triggered scripts can easily launch multiple instances of the same script simultaneously. This causes:
- Duplicate processing: The same database records or files are processed twice.
- Corrupted output: Two instances write to the same output file concurrently.
- Deadlocks or partial state: Both instances modify shared resources in an interleaved, unpredictable order.
The traditional fix was to write a PID file and check it on startup — but this approach has a race window between the check and the write. The correct modern solution is flock, which uses the kernel's advisory locking mechanism for a guaranteed race-free lock.
Locking with flock: The One-Liner Pattern
flock acquires an advisory lock on a file descriptor before running a command. The simplest usage wraps your entire script from the command line:
flock -n /var/lock/myscript.lock bash myscript.sh
-n(non-blocking): exits immediately with status 1 if the lock is already held, rather than waiting.- Without
-n,flockblocks until the lock becomes available — useful for queuing. - The lock file itself is just a marker; its content does not matter. It is safe to keep it between runs.
- When the process holding the lock exits, the kernel automatically releases it — no manual cleanup needed.
#!/usr/bin/env bash
# launcher.sh — prevents concurrent runs of worker.sh
set -euo pipefail
LOCKFILE="/tmp/myworker.lock"
if ! flock -n "$LOCKFILE" bash -c 'echo "Running worker..."; sleep 2; echo "Done."'; then
echo "Another instance is already running. Exiting." >&2
exit 1
fiflock Inside a Script Using a File Descriptor
For locking within a script rather than wrapping it from outside, use exec to open a file descriptor and then call flock on that descriptor. This is the idiomatic pattern used in production scripts.
exec 200>"$LOCKFILE"opens the file on descriptor 200 for writing (creating it if needed).flock -n 200tries to lock descriptor 200 non-blocking.- Because the lock is tied to the file descriptor (not the file name), it is released automatically when the shell process exits.
- Descriptor numbers 200-299 are conventionally used to avoid clashing with stdin/stdout/stderr.
#!/usr/bin/env bash
set -euo pipefail
LOCKFILE="/tmp/myjob.lock"
# Open lock file on FD 200
exec 200>"$LOCKFILE"
# Attempt non-blocking lock
if ! flock -n 200; then
echo "ERROR: Another instance of this script is running." >&2
exit 1
fi
echo "Lock acquired. Starting work..."
sleep 1
echo "Work complete. Lock will be released on exit."Combining mktemp and flock in One Script
Real defensive scripts need both: a lock to prevent concurrent runs and secure temp files for intermediate data. Here is the complete pattern combining both techniques:
- Acquire the lock first — before creating any temp files — so only one instance does any work at all.
- Create temp resources after the lock is confirmed.
- Register the
trapimmediately after creating temps so cleanup is guaranteed regardless of how the script exits. - The lock file never goes in the temp directory — it must persist between runs so
flockcan reference it.
#!/usr/bin/env bash
set -euo pipefail
LOCKFILE="/tmp/report_builder.lock"
exec 200>"$LOCKFILE"
if ! flock -n 200; then
echo "Already running — aborting." >&2
exit 1
fi
# Now safe to create temp resources
TMPDIR=$(mktemp -d /tmp/report.XXXXXX)
TMPLOG=$(mktemp /tmp/report_log.XXXXXX)
cleanup() {
rm -rf "${TMPDIR:-}"
rm -f "${TMPLOG:-}"
}
trap cleanup EXIT
echo "Building report in $TMPDIR" | tee "$TMPLOG"
echo "Step 1 complete" >> "$TMPLOG"
cat "$TMPLOG"Lock Directories as an Alternative Lock Mechanism
On systems where flock is unavailable (some embedded systems or network filesystems like NFS), you can use lock directories instead. mkdir is atomic on POSIX systems: it succeeds only if the directory does not already exist.
- Create the lock directory with
mkdir /tmp/myscript.lock.d— if another instance already created it,mkdirfails immediately. - Store metadata (like the PID) inside the directory for diagnostics.
- Always remove the directory in a
traponEXIT. - Caveat: Unlike
flock, a directory lock is NOT automatically released if the process is killed with-9or the machine reboots — add a stale-lock detection check.
#!/usr/bin/env bash
set -euo pipefail
LOCKDIR="/tmp/myscript.lock.d"
# Atomic mkdir — fails if directory already exists
if ! mkdir "$LOCKDIR" 2>/dev/null; then
# Check if the holding PID is still alive
HOLDER_PID=$(cat "$LOCKDIR/pid" 2>/dev/null || echo "")
if [[ -n "$HOLDER_PID" ]] && kill -0 "$HOLDER_PID" 2>/dev/null; then
echo "Locked by PID $HOLDER_PID. Exiting." >&2
exit 1
else
echo "Stale lock detected. Removing and continuing." >&2
rm -rf "$LOCKDIR"
mkdir "$LOCKDIR"
fi
fi
echo "$$" > "$LOCKDIR/pid"
trap 'rm -rf "$LOCKDIR"' EXIT
echo "Lock acquired via directory. Running..."
sleep 1
echo "Done."Timeout-Based Waiting with flock
Sometimes you want to wait for a lock rather than fail immediately — but not wait forever. flock supports a timeout with the -w flag.
flock -w 10 200waits up to 10 seconds for the lock, then exits with status 1 if still unavailable.- This is ideal for scripts that should queue behind a short-lived predecessor but give up if the predecessor is stuck.
- Combine
-wwith a meaningful error message that includes context — the lock file path and how long you waited — so operators can diagnose hangs quickly.
#!/usr/bin/env bash
set -euo pipefail
LOCKFILE="/tmp/data_sync.lock"
TIMEOUT=15
exec 200>"$LOCKFILE"
echo "Waiting up to ${TIMEOUT}s for lock on $LOCKFILE..."
if ! flock -w "$TIMEOUT" 200; then
echo "ERROR: Could not acquire lock after ${TIMEOUT}s." \
"Another instance may be hung." >&2
exit 1
fi
echo "Lock acquired. Syncing data..."
sleep 1
echo "Sync complete."Defensive Checklist: Safe Temp Resources
Before shipping any script that uses temp files or locking, run through this checklist:
- Use
mktemp, never hardcoded paths —/tmp/myapp.tmpis predictable and exploitable. - Capture the path immediately —
TMPFILE=$(mktemp ...)before any other command. - Register
trap cleanup EXITimmediately after creation — not at the end of the script. - Double-quote all variable uses —
rm -f "$TMPFILE", neverrm -f $TMPFILE. - Prefer
flockover PID files — kernel-managed, automatically released on crash. - Use non-blocking
-nby default — silent blocking locks hide performance problems. - Put the lock file outside the temp directory — so it survives the cleanup trap.
- Test cleanup behavior — run your script and
kill -9it mid-execution; verify no leftover files remain (forflock-based scripts; directory locks need extra care).
Knowledge Check: flock Flag Behavior
A cron job runs every minute and processes a shared file. You want any new invocation to exit immediately with an error if a previous run is still active, without waiting. Which flock invocation correctly implements this?
Recap: Safe Temporary Files and Lock Directories
In this lesson you learned the two essential tools for defensive resource management in Bash:
mktempcreates unpredictable, securely-permissioned temporary files (0600) and directories (0700), eliminating race conditions and symlink attacks that plague hardcoded paths.trap cleanup EXITguarantees temp file removal on any exit — normal, error-triggered, or signal-triggered — when registered immediately after creation.flockprovides kernel-enforced advisory locking: use-nto fail fast on contention,-w Nto wait with a timeout, and theexec 200>filepattern for in-script locking that the kernel releases automatically on process exit.- Lock directories (
mkdir) offer a portable fallback for environments whereflockis unavailable, but require explicit stale-lock detection. - Always keep the lock file outside the temp directory and double-quote every variable used in cleanup.
Combining mktemp + flock + trap gives you scripts that are safe against concurrent invocations, unpredictable crashes, and adversarial filesystem manipulation.
Frequently asked questions
Is the “Safe Temporary Files and Lock Directories” lesson free?
Yes — the full text of “Safe Temporary Files and Lock Directories” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.
What will I learn in “Safe Temporary Files and Lock Directories”?
Use mktemp and flock to create race-free temp resources and prevent concurrent script runs. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DevOps Bootcamp?
No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Safe Temporary Files and Lock Directories” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DevOps Bootcamp lesson?
Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Strict Mode with set -euo pipefail
- Trap Handlers for Cleanup and Signals
- Safe Temporary Files and Lock Directories
- Idempotent Scripts and Retry-with-Backoff Logic