Last reviewed: 2026-10-14
Direct answer
Prevent temporary-file races in coding-agent patches by treating creation, use, and deletion as one security contract. Reject code that first invents or checks a pathname and then opens it in a separate step. Between those operations, another process may create the path or replace it with a link. Use the runtime’s secure create-and-open primitive, retain the returned file descriptor or handle, put related files in a newly created task directory, and keep permissions, byte limits, cleanup, and promotion explicit.
The central rule is simple: uniqueness is not ownership. A timestamp, process ID, random-looking suffix, or successful existence check does not reserve a pathname. MITRE’s CWE-377 reference describes how name generation followed by a later open creates a race and can expose data, accept attacker-controlled contents, or write through a pre-created link. The fix is not a more imaginative filename. It is an atomic creation operation that fails if the path already exists.
The official runtime references provide suitable building blocks. Python’s tempfile documentation
says mkstemp() creates and opens a file without a creation race when the platform implements exclusive open correctly, restricts it to the creating user, and leaves closing and deletion to the caller. Node.js file-system documentation
documents mkdtemp() for unique directories and the exclusive-open flag, including the important warning that exclusive semantics may not work on network file systems. Go’s os package documentation
states that CreateTemp creates an open read-write file with mode 0600 before umask, while MkdirTemp creates a directory with mode 0700 before umask; callers own removal.
Those sources establish primitive behavior, not the safety of a particular patch. The contract, review steps, and test matrix below are operational recommendations. A passing unit test or secure library call does not establish safety if the patch later closes and reopens the path, moves it across trust boundaries, widens permissions, logs sensitive contents, or lets cleanup follow attacker-controlled links.
Who this is for
This guide is for maintainers reviewing agent-written code that stages patches, downloads artifacts, generates archives, buffers subprocess output, writes editor integrations, creates test fixtures, or swaps generated files into place. These paths are easy to dismiss as plumbing, yet they often combine untrusted repository data with shared filesystem locations and a worker identity that has more authority than the task needs.
Use the workflow whenever a patch touches temporary paths, cache directories, scratch workspaces, lock files, upload staging, or atomic replacement. Pair it with the site’s permission and secret boundary guide when temporary data may contain credentials, source code, prompts, customer input, or deployment output.
Key takeaways
- Create and open a temporary file in one operation. Do not generate a name, check it, and open it later.
- Keep using the returned handle. Reopening by path recreates a race and can target a different object.
- Use a newly created, task-scoped directory when a tool needs several files or insists on path-based input.
- Keep the parent directory outside repository control and inaccessible to other users or tenants where the platform permits it.
- Set the narrowest required permissions at creation time. A later
chmodleaves an exposure window. - Treat exclusive-open support as a platform assumption that must be verified, especially on shared or network filesystems.
- Bound file count, total bytes, runtime, and retained lifetime. Secure creation does not prevent disk exhaustion.
- Keep temporary paths and contents out of ordinary logs. Record a task-local identifier, size, result, and cleanup status instead.
- Do not publish, execute, or import staged output until validation completes and promotion succeeds.
- Test collisions, links, concurrency, cancellation, cleanup failure, and abrupt termination in a disposable environment.
Sources checked
- Python
tempfiledocumentation from the Python Software Foundation was fetched successfully on 2026-10-14. The live Python 3.15.0 page supports the claims about secure temporary-file and directory creation, context-managed cleanup,mkstemp()exclusive creation, creator-only access, non-inherited descriptors, and caller-owned cleanup. - Node.js file-system documentation
from the Node.js project was fetched successfully on 2026-10-14. The live v26.11.1 page supports the behavior of
mkdtemp(), disposable temporary directories,O_CREAT | O_EXCLand thexflag, symbolic-link failure on POSIX under exclusive open, and the network-filesystem caveat. - Go
ospackage documentation from the Go project was fetched successfully on 2026-10-14. The live go1.27.2 package page supports random-suffix creation, collision avoidance across concurrent callers,0600file mode,0700directory mode, and caller-owned removal. - CWE-377: Insecure Temporary File from the MITRE CWE Program was fetched successfully on 2026-10-14. The current CWE 4.20 entry supports the threat model for predictable names, name-to-open races, pre-created files, link substitution, permission exposure, corruption, and denial of service.
These are stable, current technical references rather than a dated product announcement. Version numbers and platform behavior still matter, so a reviewer should bind evidence to the runtime, operating system, filesystem, mount options, and library version actually used by the patch.
Contract details to verify
Write the temporary-storage contract before accepting an implementation. A reviewer should be able to recover every answer from the diff, tests, and a small sanitized evidence record.
| Contract field | Decision to record | Evidence to retain |
|---|---|---|
| Purpose | Why temporary storage is needed instead of memory or streaming | Data-flow note and maximum expected size |
| Parent | Which deployment-owned directory contains the object | Parent policy ID, filesystem type, and owner class |
| Creation | Exact create-and-open or create-directory primitive | Runtime and library version plus flags or API name |
| Permissions | Initial file and directory access | Requested mode and observed effective mode in a disposable test |
| Handle use | Whether later I/O uses the original handle | Trace from creation through close; every path reopen justified |
| Naming | Which values influence a prefix or suffix | Fixed prefix policy; sensitive or repository-controlled values excluded |
| Contents | Data classification and executable status | Redacted class, byte count, and whether execution is prohibited |
| Budget | Maximum files, bytes, age, and concurrent tasks | Boundary-test results and stable rejection codes |
| Promotion | How validated output becomes visible | Same-filesystem replacement rule and destination validation |
| Cleanup | Owner for success, failure, cancel, crash, and restart | Cleanup result, retention exception, and later sweeper evidence |
Create the object before exposing its name
Prefer a high-level secure temporary-file or directory API. It should combine unpredictable naming with exclusive creation and return an already open object. If the application needs a deterministic final name, keep the temporary name private and make the final name part of a separate validated promotion step.
Do not accept patterns such as exists(path) followed by open(path), mktemp() followed by an ordinary create, or a timestamp joined to a shared directory. Locks do not repair the design unless every possible actor honors the same lock, which an attacker does not. A random value reduces accidental collisions but still does not establish atomic ownership.
For Node.js code that must create a particular child name inside a private task directory, the documented exclusive flag gives the required fail-if-present behavior:
import { mkdtemp, open, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
async function stageResult(encodedResult, reportCleanupFailure) {
const workDir = await mkdtemp(join(tmpdir(), 'agent-task-'));
let file;
let primaryError;
try {
file = await open(join(workDir, 'result.json'), 'wx', 0o600);
await file.writeFile(encodedResult);
await file.sync();
// Validate through this handle before any separate promotion step.
} catch (error) {
primaryError = error;
throw error;
} finally {
const cleanupErrors = [];
try { await file?.close(); } catch (error) { cleanupErrors.push(error); }
try { await rm(workDir, { recursive: true }); } catch (error) { cleanupErrors.push(error); }
if (cleanupErrors.length > 0) {
if (primaryError) reportCleanupFailure(cleanupErrors.length);
else throw new AggregateError(cleanupErrors, 'temporary cleanup failed');
}
}
}
This is an illustrative shape, not a universal safe wrapper. The real patch must handle partial creation, close and cleanup errors, cancellation, platform support, byte limits, and final promotion. It must also avoid a network filesystem if the deployment cannot provide the exclusive semantics on which the design relies.
Keep the original handle authoritative
After secure creation, use the returned handle for writing, flushing, validating, and, where supported, obtaining metadata. Closing it and reopening the stored pathname lets another object occupy the name if the surrounding directory is not fully private. Passing a pathname to an external tool is also a trust transition: document why the tool needs it, which identity runs the tool, and whether another actor can alter either the directory or the object while the tool works.
When a tool only accepts paths, create a private task directory with a secure primitive, create children exclusively, and run the tool with no broader write access than necessary. Keep plugins, startup files, and executable search paths out of that directory. The related command-injection review guide covers the separate process-launch boundary.
Separate validation from promotion
Temporary output should remain non-live until the application validates its format, size, content class, and expected producer. If promotion uses rename or replacement, verify that the source and destination semantics match the intended operating systems and filesystems. A same-filesystem atomic rename can help readers avoid partial content, but it does not validate the destination, authorize overwriting, or make cross-filesystem moves atomic.
For extracted or generated trees, validate every entry before promotion and keep consumers from watching the staging directory. Link handling requires extra care because a later child operation can follow a link created earlier. The site’s archive-extraction containment guide develops that broader rooted-filesystem problem.
Make cleanup a state machine
Define cleanup for each terminal state: creation failed, write failed, validation failed, promotion failed, request canceled, process terminated, and restart found abandoned state. Automatic cleanup helps, but the owner still needs to observe failures. A destructor or garbage collector is not a deadline, and a broad periodic deletion job can remove another task’s active files if ownership is inferred only from a name.
Use a task identifier stored outside the filename as the ownership record. A sweeper should operate only under the approved scratch root, reject links and unexpected file types, check age and active-task state, and cap work per run. If forensic retention is required, move evidence through a controlled promotion path into a restricted store rather than leaving it in a shared temporary directory.
Build a harmless race-test matrix
Test the adapter behind the application’s temporary-storage interface, then add a small integration suite on every supported filesystem class. Use a disposable parent created for the test. Never aim hostile fixtures at a developer checkout or a shared system directory.
| Case | Expected result |
|---|---|
| Many concurrent creators with one prefix | Every successful call owns a distinct open object |
| Candidate name already exists | Creation fails or chooses a new secure name; existing bytes are unchanged |
| Candidate path is a symbolic link | Exclusive child creation fails; link target is unchanged |
| Prefix contains repository text or separators | Rejected or reduced to a fixed application-owned prefix |
| Parent has broader access than policy allows | Task fails before sensitive bytes are written |
| Write exceeds the byte budget | Controlled failure, closed handle, and bounded cleanup |
| Cancellation races with close | One terminal state, no use-after-close, and cleanup is attempted once |
| Validation fails after a complete write | No promotion; staged object is removed or quarantined by policy |
| Cleanup itself fails | Original result is preserved, cleanup failure is recorded, and bounded recovery is scheduled |
| Restart sees an abandoned task directory | Ownership is reconciled before deletion; active or unknown state fails closed |
For the link test, create an inert outside sentinel and point a test link at it. Assert that the sentinel’s bytes, mode, and modification time do not change. For the collision test, fill the existing file with a marker and verify the marker remains intact. Do not claim the matrix passed unless it ran on the exact runtime and filesystem represented in the evidence.
Happy-path operator workflow
- Inventory every temporary-file and scratch-directory call touched by the patch, including wrappers and library helpers.
- Trace the parent directory, prefix, suffix, and content back to their origins. Treat repository names, issue text, model output, upload metadata, and user identifiers as untrusted.
- Replace name-only generators and check-then-open sequences with the runtime’s secure create-and-open or create-directory primitive.
- Keep the returned handle through writing, flushing, and validation. Mark every conversion back to a pathname for explicit review.
- Set file count, byte, duration, and concurrency budgets before writing. Reject work that cannot fit within them.
- Run the worker with a minimal identity, environment, and filesystem scope. Confirm the task directory is not on a filesystem that breaks required exclusive semantics.
- Validate staged output before visibility. Define the destination collision policy and do not silently overwrite an unrelated file.
- Close handles and clean the task directory on success. Record only the task identifier, size bucket, result, promotion outcome, and cleanup outcome.
- Run the race-test matrix under concurrent load on each supported operating-system and filesystem class, then review the final diff for fallback paths that use ordinary opens.
A useful successful evidence record contains a policy version, runtime version, filesystem class, creation primitive, effective permission class, file count, byte count, validation result, promotion result, cleanup result, and test-run identifier. It should not contain full temporary paths, contents, prompts, credentials, or raw tool output.
Error-path operator workflow
If secure creation is unavailable or the filesystem cannot provide the assumed semantics, stop before writing. Do not fall back to a predictable name, weaken exclusive behavior, or retry with a normal open. Return a stable code such as secure_temp_unavailable, parent_policy_failed, or exclusive_create_unsupported and route the task to a deployment decision.
If writing or validation fails, keep the object non-live, close the handle, and attempt cleanup once through the task owner. Preserve the primary failure separately from cleanup failure so a deletion error does not hide the cause. If policy requires retained evidence, record a controlled quarantine decision; do not leave the object in place and call that quarantine.
If promotion finds an existing destination, follow the predeclared collision policy. For generated cache data, discard and recompute may be correct. For a user-visible patch or artifact, an existing destination should normally stop the operation for reconciliation. Never delete or replace the destination merely because its name resembles the task’s expected output.
If cancellation or shutdown interrupts the task, close new writes, wait for governed child processes, and transition ownership to cleanup or restart reconciliation. An unknown state must not trigger blind deletion. Record the exact task identity and next action so a later process can distinguish abandoned scratch data from active work.
Failure modes
A random filename is treated as a reservation. Randomness lowers collision probability but does not make selection and opening atomic. Use a primitive that creates the object and returns its handle.
The patch checks that the path is absent first. That check creates the gap an attacker or concurrent worker can win. Make existing-path failure part of the create operation.
Permissions are tightened after writing. Another identity may read or alter the object before the change. Request restrictive access during creation and verify the effective result.
A secure file is reopened by name. The initial creation may be sound while later validation, upload, or cleanup reaches a substituted object. Retain the handle or prove the private-directory boundary.
The shared parent is assumed trustworthy. System temporary directories are often intentionally shared. A private child directory reduces exposure only if its creation and permissions are secure and later code does not widen them.
Exclusive open is assumed to work everywhere. Node.js explicitly documents a network-filesystem caveat. Bind the design to tested deployment storage and fail closed on an unsupported mount.
Cleanup recursively follows unexpected objects. A name-based sweeper can cross a link or delete another task’s files. Constrain cleanup to the approved root, reject links and special files, and reconcile ownership before removal.
Temporary output becomes executable. Generated scripts, package hooks, configuration, or binaries can gain authority when another tool discovers them. Keep scratch roots out of executable, plugin, module, and startup search paths.
Logs become the durable copy. Recording a full path, file contents, or tool stderr may preserve the sensitive data longer than the temporary object. Log classifications, counts, and reason codes instead.
A cleanup success hides a failed promotion. Deleting the staged file says nothing about whether the destination was correct. Record validation, promotion, and cleanup as separate outcomes.
FAQ
Is a UUID filename sufficient?
No. It may make guessing difficult, but it does not atomically claim the path or define permissions, cleanup, and promotion. Use a secure creation primitive even when the prefix or suffix includes a UUID.
Should every task use a temporary directory?
Not necessarily. A single anonymous or securely created file may be simpler when the consumer can use an open handle. Use a private directory when several files must share a boundary or a tool requires path-based input, then govern the whole directory lifecycle.
Is O_EXCL all the patch needs?
No. It addresses existing-path collision when combined correctly with creation, subject to platform and filesystem behavior. The patch still needs a trusted parent, restrictive access, handle discipline, resource budgets, validation, cleanup, and safe promotion.
Can a periodic sweeper replace in-process cleanup?
No. In-process cleanup should handle known terminal states promptly. A sweeper is recovery for abandoned state and must reconcile ownership carefully. It should not guess from filename patterns alone.
What should a reviewer retain?
Retain the policy and runtime versions, filesystem class, test fixture digest, result for each matrix case, effective permission class, byte and file counts, validation and promotion outcomes, cleanup outcome, and any explicit platform gap. Avoid retaining staged contents or sensitive absolute paths.
Reader next step
Choose one temporary-file path introduced or modified by a coding agent. Write its contract table, then add three disposable fixtures before changing production code: a pre-existing file with marker bytes, a symbolic link to an outside sentinel, and 100 concurrent creators using the same fixed prefix. Replace any name-then-open sequence with the runtime’s secure primitive, retain the returned handle, and prove that collisions fail safely, the sentinel remains unchanged, every successful creator owns a distinct object, and cleanup leaves no unknown state.
After that narrow gate passes on every supported filesystem class, review the same patch for process-launch and archive-extraction boundaries. Temporary storage is often the handoff between those two authority surfaces, so all three contracts must agree on ownership, validation, and promotion.