ErrLookup › Background 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
- Ownership mismatch (root-owned files, wrong uid). A tool was run as root or sudo — or a container with a fixed uid (Coolify's 9999) — leaving files or directories owned by another user, so the regular serving/running user cannot write. This is the top cause across the family: Grav's user/config, Coolify's /data/coolify/ssh, ~/.config/gws, .astro, and ~/.waveterm all document it. Fix with chown to the actual running user and avoid mixing users.
- Unwritable or missing parent directory. Creating or replacing a file requires write permission on the containing directory; a missing directory, a 755 root-owned directory, or a path component that is actually a file (ENOTDIR/EISDIR) all fail the write. Atomic rename strategies make the directory requirement explicit — the temp file must be created in the same directory.
- Full disk or quota exhausted (ENOSPC). Small writes fail hard when the filesystem is full or over quota — documented everywhere from Coolify SSH keys to local-model report runs and CI codegen steps. Check df -h on the volume actually holding the target path.
- Read-only filesystem or mount. Containers mount volumes read-only, state directories sit on read-only layers, or the filesystem was remounted read-only. Every write into such a path fails regardless of permissions; the fix is remounting or pointing the library's configurable path at a writable volume.
- File locked by another process. Editors, sync clients, antivirus scanners, or concurrent instances hold exclusive locks (especially on Windows), breaking both plain writes and atomic renames — Grav and Astro both document rename failures caused by scanners, and claude-mem documents settings.json locked by Claude Desktop. Close the holder and retry.
- Security policy denials (open_basedir, SELinux, AppArmor, sandboxes). The OS or runtime denies the write even with correct Unix permissions: PHP open_basedir excluding the cache path, SELinux/AppArmor blocking a daemon's write, or sandbox/seatbelt rules in macOS blocking writes outside allowed directories. Check the relevant audit logs when chown/chmod doesn't help.
- Wrong or unhelpful configuration of the output path. A configurable storage root (Coolify's ssh-keys disk, GOOGLE_WORKSPACE_CLI_CONFIG_DIR, WAVETERM_HOME) points somewhere unwritable or unset — e.g. HOME missing in a service context. Verify what path the library actually resolved before fixing the filesystem.
What usually fixes it
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
Documented occurrences
- Failed to commit nonce key file (getgrav/grav)
- Failed to store SSH key: {message} (coollabsio/coolify)
- Failed to open lock file for SSH key: {$lockFile} (coollabsio/coolify)
- ⚠️ Could not save report: ${err.message} (santifer/career-ops)
- SSH keys storage directory is not writable. Run on the host: sudo chown -R 9999 /data/coolify/ssh && sudo chmod -R 700 /data/coolify/ssh && docker restart coolify (coollabsio/coolify)
- Failed to create SSH keys storage directory (coollabsio/coolify)
- failed to copy pyproject.toml for workspace package %s: %w (anomalyco/sst)
- remove legacy AI editor actions failed: %w (siyuan-note/siyuan)
- ⚠️ Could not save HTML: ${err.message} (santifer/career-ops)
- ⚠️ Could not save report: ${err.message} (santifer/career-ops)
- [uninstall] ${label} cleanup failed: (thedotmack/claude-mem)
- identity: write proxy secret: %w (gastownhall/beads)
- writing file %s: %w (golangci/golangci-lint)
- saving configuration file: %w (golangci/golangci-lint)
- failed to save wg private key: %w (cilium/cilium)
- [uninstall] Could not write settings during server runtime cleanup: (thedotmack/claude-mem)
- error writing zsh-integration .zlogin: %v (wavetermdev/waveterm)
- Failed to create token directory '{}': {} (googleworkspace/cli)
- writing output file: %w (cilium/cilium)
- Cannot save response to %s: %s (w7corp/easywechat)
…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.