ErrLookup › Background articles › "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk
"failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk
"Failed to read file", "could not read <path>", ENOENT, EACCES and "Unable to read" errors all come from one place: a program tried to open and read a file and the OS said no. This article explains the file-read failure family across open-source tools — the read happens, then what — and how to tell a missing file from a permission problem, a race, or a corrupt/unreadable one.
Distilled from 106 documented records across 49 repositories.
Background
Every error in this family starts at the same boundary: a call to a file-read primitive — fs.readFile in Node, std::fs::read_to_string in Rust, File.read or File.open in Ruby, file_get_contents in PHP, io.Copy or io.ReadAll in Go — and the OS or the decoder returning a failure. The message you see is almost never the OS error verbatim; it is a wrapper each library adds so the failure is identifiable in context: puppetlabs/puppet names the filetype class and path ('%{klass} could not read %{path}'), HumanSignal/label-studio embeds str(e) after 'Failed to read file {path}', yamadashy/repomix appends the underlying error.message, tailscale's TKA storage wraps every read phase with '%w' so the errno is preserved inside. The first debugging step is almost always to find and read the wrapped inner message, because the fix for EACCES (chmod/chown) differs completely from the fix for ENOENT (restore the file) or EISDIR (you passed a directory where a file was expected).
Despite the shared primitive, the family splits into distinct behavioral groups. One group fails hard: facebook/flow's 'check-contents input should be readable' and content_of_file_input_unsafe, and zed's 'Failed to read path' all use expect/unwrap/panic, turning an ordinary missing or non-UTF-8 file into a process crash — flow even ships a Result-returning twin (content_of_file_input) whose use avoids the panic entirely. Another group degrades gracefully: eyaltoledano/claude-task-master catches read failures in its ContextGatherer and downgrades them to console.warn while continuing, and repomix's readRawFile returns { content: null, skippedReason: 'encoding-error' } so the pipeline skips the file. Whether a read failure is fatal is a design choice by the library, not a property of the error itself.
A third pattern is the race: the file existed at scan, glob, or existsSync time but was gone (or changed) by the time the read executed. Repomix's trust prompt reads a config file that was just lstat'd; sst reads a pyproject.toml that os.Stat saw moments earlier; beekeeper-studio reads enums.json right after existsSync passed; tailscale's compaction re-reads AUM records that AllAUMs listed moments before and can vanish to a concurrent commit or purge. These TOCTOU races produce the same wrapper messages as plain missing-file errors, so a transient failure deserves one retry before deep debugging.
Finally, some members of this family are not about missing or unreadable files at all but about unreadable content: flow's read_to_string panics on non-UTF-8 bytes, siyuan distinguishes plain I/O errors from 'source changed' sentinels, and vitess's 'can't read init-db-sql-file' fires only when the open succeeded but the mid-stream read failed (EIO, or a special device that errors on read). Note that messages can also mislead: getgrav/grav's 'Bad Data' from DataFile::load sounds like corrupt content but actually means file_get_contents returned false — a failed read — while tailscale's PurgeAUMs 'reading %d (%x)' deliberately does not tolerate os.ErrNotExist, whereas its retainStateCandidate path does. Treat each library's wrapper semantics as its own contract and check the record page for specifics.
Common causes
- File missing or deleted mid-operation. The most common trigger across records: the path does not exist (ENOENT), or existed at scan/glob/stat time but was deleted before the read — repomix's temp-clone races, sst's stat-then-read race, task-master's files deleted between dependency detection and reading. Retrying is often enough when the cause is a cleanup race.
- Permission denied. The file exists but the process user cannot read it (EACCES/EPERM): wrong ownership or mode, container UID mismatch (label-studio, task-master), restrictive directories, or open_basedir blocks in PHP (grav). Fix with chmod/chown or run as a user with access.
- Wrong kind of path or non-regular file. A directory, broken symlink, or special device was passed where a regular file was expected: puppet records a directory case, sst and xai-org/grok-build mention directories and dangling symlinks, vitess hits errors reading character devices. Verify with stat/ls -l that it is a readable regular file.
- Race with a concurrent writer. A second process deleted, truncated, or locked the file while it was being read: tailscale's compaction racing commits/purges on the same chonk, slim's layer directory mutated mid-build, beekeeper-studio's enums.json locked or removed by sync tools. Single-owner directories or serialization fix the class of problem.
- Non-UTF-8 or undecodable content. Rust's read_to_string and Node's encoding handling reject invalid byte sequences: flow panics on non-UTF-8 input, zed's example loader rejects UTF-16/Latin-1 logs, repomix reports 'encoding-error' skips. Transcode explicitly (iconv) or read bytes and decode deliberately.
- Corrupt stored record failing decode. For parsers that read structured formats (tailscale's CBOR AUM files, x-algorithm's parquet footers), the open succeeds but the content will not decode, failing the whole scan — a single bad file anywhere in tailscale's chonk fails Heads() and ChildAUMs(). Quarantine the file named in the wrapped error.
- Hardware or filesystem-level I/O failure. The rarest cause: EIO from a failing disk, full filesystem, NFS hiccup, or tmpfs too small (freika/dawarich's streamed multi-GB import, vitess's init SQL read). Check dmesg/smartctl and free space; these are often transient but recur until the storage is fixed.
What usually fixes it
- Decode the wrapped inner error first: every library in this family preserves the OS error (ENOENT vs EACCES vs EIO) inside its message — the correct fix is entirely determined by which one it is.
- Verify the target is a readable regular file before re-running: stat/ls -l for existence, type, and permissions, and test access as the actual process user (sudo -u <user> test -r) to catch container/UID mismatches.
- Retry once before deep debugging when the read follows a scan, glob, or exists check — stat-then-read and temp-dir cleanup races are a recurring pattern and frequently transient.
- Eliminate concurrent access to read targets: one writer process per directory, serialize builds/compaction, and copy or restore state directories only while the owning daemon is stopped.
- Handle encoding explicitly: transcode non-UTF-8 inputs upstream (iconv, re-export to UTF-8 markdown) rather than letting read-to-string/readFile reject them.
- Where the library offers a graceful alternative — a Result-returning read instead of an unsafe panic variant, a FileContent payload instead of a FileName path, or skip-and-continue semantics — prefer it so ordinary I/O failures do not become crashes.
Go deeper
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Documented occurrences
- check-contents input should be readable (facebook/flow)
- Failed to read CV file at ${customCvPath}: ${err.message} (santifer/career-ops)
- marking young AUMs: %w (tailscale/tailscale)
- reading heads: %v (tailscale/tailscale)
- content_of_file_input_unsafe: failed to read file (facebook/flow)
- reading children %v: %w (tailscale/tailscale)
- reading %d (%x): %w (tailscale/tailscale)
- marking descendant AUMs: %w (tailscale/tailscale)
- Bad Data (getgrav/grav)
- Could not read the remote repository's config (${configName}) for review: ${error instanceof Error ? error.message : String(error)} (yamadashy/repomix)
- ${message} || Unable to read ${sectionPath} (can1357/oh-my-pi)
- reading active chain (retainStateCandidate, %v): %w (tailscale/tailscale)
- reading active chain (retainStateActive) (%d, %v): %w (tailscale/tailscale)
- Failed to read JSON data: %{message} (Freika/dawarich)
- Requested file can currently not be accessed. (nextcloud/server)
- %{klass} could not read %{path}: %{detail} (puppetlabs/puppet)
- 345: read Markdown [%s]: %w (siyuan-note/siyuan)
- Warning: Could not generate project tree: ${error.message} (eyaltoledano/claude-task-master)
- Failed to read file {path}: {str(e)} (HumanSignal/label-studio)
- failed to read {}: {e} (xai-org/grok-build)
…and 86 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.