ErrLookupBackground articles › "Permission denied" / "Failed to write" file errors: why a library can't write its files to disk (EACCES, EPERM, ENOSPC) and how to fix them

"Permission denied" / "Failed to write" file errors: why a library can't write its files to disk (EACCES, EPERM, ENOSPC) and how to fix them

"Failed to write file: Permission denied" and similar messages (EACCES, EPERM, ENOSPC, "failed to open stream", "read-only file system") appear when a library tries to persist a file — a config, secret key, generated code, report, or credential cache — and the operating system refuses the write. This family covers 108 documented cases across 43 projects: why the write fails, how to read the wrapped OS error, and how to fix ownership, permissions, disk space, locks, and read-only mounts so the write succeeds.

Distilled from 108 documented records across 43 repositories.

Background

These errors are produced at the lowest layer of the stack — the OS rejecting an open(), write(), or rename() — and then wrapped by whatever library was trying to persist something. A write-permission denial is rarely about the file alone: writing a file requires write permission on the file itself (or permission to create it, which requires write permission on the containing directory), and atomic-write strategies additionally need to create a sibling temp file and rename it into place. That is why so many libraries in this family — Grav's nonce key commit, Coolify's SSH key storage, beads' proxy.secret, Phalcon's router cache, Google Workspace CLI's token and credential files, Astro's data store — fail on the rename or probe step rather than the content write. The failure at rename time means the directory is the thing that is unwritable, not just the target file.

From the caller's side, the message you see is usually a wrapper: "writing file %s: %w" (golangci-lint), "failed to save wg private key: %w" (Cilium), "Failed to write credentials: {e}" (googleworkspace/cli). The actionable part is almost always the wrapped OS error or chained exception, not the wrapper text. Some libraries deliberately embed remediation in the message (Coolify prints the exact chown/chmod/restart command; deepagents points at disk space and parent-directory permissions), while others bury the cause — easywechat's saveAs() embeds the response body in the message and puts the real PHP warning ("failed to open stream: Permission denied") in the previous exception, and Astro wraps everything as UnknownFilesystemError with the actual code only in error.cause.

What varies across the family is mostly whose user is doing the writing and where. The single most common shape is a user mismatch: a tool was run once as root (or as a container with a fixed uid like Coolify's 9999) and left root-owned files or directories behind, so the later run as the normal user cannot write. Containers add read-only mounts and volume-bind ownership problems; CI runners add full disks and quotas; Windows and file-sync/antivirus clients add exclusive locks that break writes and atomic renames. Hardened PHP setups add open_basedir, and SELinux/AppArmor can deny writes even when ownership and permissions look correct.

Failure semantics also differ and matter for recovery. Several libraries make the write atomic (temp file + rename, or lock-file protected writes), so a failure leaves the previous file intact and nothing half-written — a plain retry after fixing the cause is safe, and Coolify even rolls back the database row in the same transaction. Others write non-atomically with truncate-then-write semantics (claude-mem rewriting shell rc files), where a mid-write failure can leave a partially written file and you should restore from backup. Whether a retry is cheap also varies: re-running an Ollama-backed evaluation is free, while re-running an OpenAI-backed one costs API tokens, so salvaging output from stdout first is the documented advice.

Common causes

What usually fixes it

Documented occurrences

…and 88 more across the corpus — use search.

Honest provenance: generated on 2026-09-02 from AI-assisted analysis of the linked records. See how records are made.