Skip to content
Shell & TerminalHow-To Published Updated 3 min readViews unavailable

How to Create Temporary Files Safely from a Shell Script

A threat-aware shell temporary-file pattern covering private directories, mktemp portability, permissions, traps, atomic replacement, signals, and cleanup.

A predictable name such as /tmp/report.$$ is not safe merely because a process ID changes. Another user can guess names, create symlinks, or race existence checks in a shared directory. The safe pattern asks the operating system/tool to create an unpredictable object atomically, restricts access, and cleans it without turning a variable into shell source.

Prefer one private temporary directory

On systems with a trustworthy mktemp, create a directory and put all working files beneath it:

umask 077
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/report.XXXXXXXX") || exit 1

cleanup() {
    status=$?
    trap - EXIT HUP INT TERM
    rm -rf -- "$tmpdir"
    exit "$status"
}
trap cleanup EXIT HUP INT TERM

work=$tmpdir/work
result=$tmpdir/result

mktemp is widely available but not specified by POSIX, and template rules/options differ among implementations. Validate this invocation on every supported platform. For strict portable application code, a small helper using mkdtemp()/mkstemp() is safer than reimplementing randomness and O_EXCL in shell.

Trust TMPDIR only in the threat model where its owner and permissions are acceptable. A privileged script should not blindly create sensitive data in a caller-controlled directory. Drop privilege first or use a root-owned runtime location with explicit lifecycle.

Quote paths and constrain cleanup

Set tmpdir only after successful creation. Quote every expansion and pass -- to tools that support it so a leading dash is not parsed as an option. The cleanup function removes exactly the directory returned by mktemp; it does not construct a wildcard such as /tmp/report.*.

The trap captures the original status before rm and exits with it, so a successful cleanup cannot turn a failed operation into success. Clearing traps first avoids recursion. Note that SIGKILL, power loss, and kernel failure cannot run shell cleanup; a system temporary-directory service or startup scavenger must handle abandoned objects.

Avoid storing secrets longer than necessary. Permissions prevent other ordinary users from opening the directory, but root, backups, swap, crash dumps, or disk recovery may still expose content. Use an in-memory credential channel when the target supports one.

Make output replacement atomic

For a file intended to replace a destination, create the temporary file in the destination directory—not /tmp—so a final rename stays on one file system:

dest=/srv/data/index.txt
destdir=${dest%/*}
[ "$destdir" != "$dest" ] || destdir=.
stage=$(mktemp "$destdir/.index.tmp.XXXXXXXX") || exit 1

if generate >"$stage" && validate "$stage"; then
    chmod --reference="$dest" "$stage" 2>/dev/null || :
    mv -f -- "$stage" "$dest"
else
    rm -f -- "$stage"
    exit 1
fi

The permission-copy command is GNU-specific and the policy is incomplete for ownership, ACLs, xattrs, durability, and concurrent readers; use platform-appropriate tooling. mv atomicity applies to the directory entry on one file system, not automatically to disk persistence after sudden power loss.

Symlink handling at the final destination needs a written policy. A privileged script should usually reject an unexpected symlink or resolve an approved parent before staging; blindly copying metadata from a caller-controlled destination can follow or query the wrong object. If multiple writers update the same file, atomic rename prevents readers from seeing partial bytes but does not prevent lost updates. Add an application-level lock or compare-and-swap/version check, and define how stale lock state is recovered.

For durable configuration, flush the staged file and then its containing directory with a native helper before declaring a power-loss-safe commit. Portable shell utilities do not expose a universal fsync operation, so state that durability guarantee honestly.

Test hostile names and interruption

Run with TMPDIR containing spaces, a full/unwritable directory, simultaneous instances, signals during each stage, failed generators, and destinations beginning with a dash. Check modes with stat, confirm no partial destination appears, and verify the original remains after validation failure.

Temporary-file safety is a lifecycle: atomic private creation, bounded ownership, quoted use, same-filesystem commit where required, exact cleanup, and recovery for cleanup that never ran. Random-looking filenames alone provide only appearance.

Related:

Sources:

Comments