ErrLookup › Background articles › "File not found" and ENOENT errors: why libraries can't find a file that should exist
"File not found" and ENOENT errors: why libraries can't find a file that should exist
"File not found" errors — ENOENT in Node, FileNotFoundException in Java, PHP "not found" exceptions, and 404 aborts — fire when a library's resolved path does not exist on disk. A developer meets this family when a CLI argument points nowhere, a template or asset name breaks the naming contract, a relative path resolves from an unexpected working directory, or a file was deleted after something started referencing it. This article covers the shared mechanics across the family's documented records: how path resolution, sanitization, case sensitivity, symlinks, and stale references produce these errors, and which fixes generalize across libraries.
Distilled from 110 documented records across 50 repositories.
Background
The family sits at the boundary between a path a library computed and the filesystem it asked for. Nearly every record has the same shape: something resolves a name to a concrete path — a CLI validator expanding an @file argument (spree), a template resolver gluing prefix, name, and format together (career-ops), a config plugin reading a service-discovery file at configure time (fluentd), a native loader opening input before parsing (swc's parseFileSync and transform), or a storage layer emulating GridFS over Postgres rows (ruflo) — and the existence check fails. What varies is when the check runs: fluentd reads the file at configure time because it builds the initial server list then, Puppet's init service provider resolves the script lazily at first command execution, and claude-mem checks mode files at load time. Some libraries check one path; others walk a candidate list and only fail when every candidate misses, as claude-mem's mode directories and claude-mem's MCP server script resolver do.
From the caller's side the message almost always names the expected path or name, which is the family's most useful property: career-ops prints the exact filename its template resolution landed on, claude-mem lists every directory it searched, and caveman echoes the offending input. But severity is library-specific: some throw and abort before any work starts (wallabag's import command), some downgrade to a warning with a fallback — claude-mem falls back to the built-in 'code' mode, and RocketChat removes the app record while only warning about the missing package file — and some defer the failure into another layer entirely. In native bridges the file-load failure can surface as a panic-converted JavaScript exception whose text does not name the file at all (the swc records carry unrelated decorator and duplicated-methods message text), so the missing-file cause is easy to miss unless you know the string overload means a path.
The recurring traps are structural. Relative paths resolve against whichever working directory the process happens to have — different between web and CLI PHP runtimes, between rspec, rake, spring, and IDE runners, and between write time and read time (ruflo sealed trajectories silently redirect if cwd changes) — while career-ops resolves photo paths against the script's own directory rather than the caller's cwd. Sanitizers can change the lookup key before the check: bagisto and anything-llm strip traversal-shaped segments so the searched name differs from the URL name, and career-ops kebab-cases template names before building the filename. Filesystem features add more: a missing public/storage symlink silently removes a lookup base in bagisto, dangling symlinks read as nonexistent in gradle because File.exists() follows links, and deno's embedded VFS is case-sensitive on Linux targets where the host filesystem a developer tested on may not be.
A distinct sub-family treats "file" as a record rather than a disk path. ruflo's Postgres backend throws "File not found" when the files-table row is gone while conversation metadata still references the id; RocketChat's GridFS-backed app packages hit "File not found for id" when another cluster instance wins the delete race during an uninstall; bagisto's download controller returns 404 when the private-disk payload behind an already-paid purchased link was deleted. These are referential-integrity failures that look identical to filesystem misses, and their fixes — cleaning dangling references, reconciling records, re-capturing artifacts — differ from simply creating the file.
Common causes
- Wrong or mistyped path, usually relative, resolved from an unexpected working directory. The dominant trigger across the family: cwd differs between web and CLI runtimes, between test runners, between containers and hosts, or between the process that wrote a file and the one reading it. spree's @payload.json, wallabag's filepath, fluentd's service-discovery path, capybara fixtures, deno WASI preopens, caveman imports, and swc file arguments all fail this way.
- File deleted, moved, or renamed after something began referencing it. Stale references outlive their targets: search-index paths to deleted files (GitNexus), enabled script entries pointing at deleted files (CodexPlusPlus), conversation metadata referencing file rows removed by cleanup or TTL (ruflo), and purchased download links whose payload is gone (bagisto).
- Name mismatch against a naming contract or case-sensitive filesystem. Template names are kebab-cased and format-suffixed (career-ops), init script filenames must match the service title exactly with no .service suffix (puppet), and README.md versus readme.md fails on case-sensitive filesystems and deno's case-sensitive embedded VFS.
- Missing or broken symlinks. A missing public/storage symlink silently removes a lookup base in bagisto; gradle's existence check follows links so a dangling symlink reports as nonexistent; CodeWhale plugin components fail as hard errors when shipped as broken symlinks.
- Sanitization mangles the lookup key. Traversal-stripping sanitizers (bagisto imagecache, anything-llm safeFilename) change the name actually searched so it differs from the URL, and kebab-casing (career-ops) transforms 'My Fancy' into my-fancy before the filename is built.
- Permissions, locks, or missing extensions masquerading as not-found. EACCES on a parent directory fails deno preopen stats; open_basedir makes file_exists() fail silently in PhpSpreadsheet; a PHP build without the fileinfo extension makes even valid images classify as not found; Windows file locks make enabled scripts unreadable at bundle time (CodexPlusPlus).
- Partial or incomplete installs and builds. Missing bundled files rather than wrong paths: plugin installs without their modes or scripts directories (claude-mem), split browser-script parts from mixed versions (impeccable), tailscaled.exe auto-updating with no tailscale.exe beside it (tailscale), plugin components never shipped (CodeWhale), and compiled binaries missing assets only referenced by runtime-computed strings (deno compile).
- Races between check and use. TOCTOU deletes between scan and bundle or between existsSync and readFile (CodexPlusPlus, ruflo), and multi-instance clusters where another node wins the delete race during an uninstall (RocketChat).
What usually fixes it
- Read the exact path or name straight from the error message and verify it with ls or an existence check before changing anything — the message is the resolution outcome the library actually used, not a guess.
- Replace relative paths with absolute ones (path.resolve, realpath, File.expand_path, Rails.root.join) anchored to an explicit base directory, so working-directory drift between runtimes, runners, and containers cannot redirect resolution.
- Re-sync references with the filesystem: re-run the scan, index, or analyze step; refresh manifests; rebuild bundles; re-seal or re-capture missing artifacts; or clean dangling ids from metadata when the underlying file is legitimately gone.
- Produce the expected artifact at the documented path and naming contract: create the file with the exact prefix.name.format, run the build, sync, or storage:link step, recompile with include globs covering runtime-computed assets, or reinstall the package as a complete unit.
- Choose explicit degradation where the library offers it: fallback flags (career-ops fallback: true), built-in fallback modes (claude-mem's 'code'), cache-miss treatment (re-capture trajectories instead of failing replay), and 404 responses instead of unhandled 500s on download routes.
- Add pre-flight checks: test -f guards in entrypoints and scripts, existence sweeps over configured paths after moves or renames, and validation of admin-entered paths and URLs at save time rather than read time.
Go deeper
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Documented occurrences
- Template not found for kind=${kind} name=${chosen} (${fileFor(chosen)}) (santifer/career-ops)
- trajectory envelope not found: ${path} (ruvnet/ruflo)
- One or more file(s) specified did not exist: %{missing_files_list} (puppetlabs/puppet)
- ROW_NOT_FOUND: Tracker not found at ${trackerPath} (santifer/career-ops)
- file or url for option '%{arg}' cannot be opened: %{value0} (puppetlabs/puppet)
- Image not found. (bagisto/bagisto)
- Mode file not found: ${modeId}.json (searched: ${this.modeDirs.join(', ')}) (thedotmack/claude-mem)
- throw new Error({}); (BigPizzaV3/CodexPlusPlus)
- Live browser script part missing: ${part.name} (${part.path}) (pbakaus/impeccable)
- Request body file not found: ${file} (spree/spree)
- Profile photo not found or unreadable: ${photo} (${err.code || err.message}) (santifer/career-ops)
- File not found (abhigyanpatwari/GitNexus)
- Decorators can't be placed on different accessors with for the same property (${element.key}). (swc-project/swc)
- File $path not found! (PHPOffice/PhpSpreadsheet)
- MCP server script not found (thedotmack/claude-mem)
- cannot find tailscale.exe alongside %s: %w (tailscale/tailscale)
- File '%s' not found. (gradle/gradle)
- This instance could not remove the ${item.info.name} app package. If you are running Rocket.Chat in a cluster with multiple instances, possibly other instance removed the package. If this is not the case, it is possible that the file in the database got renamed or removed manually. (RocketChat/Rocket.Chat)
- Duplicated methods (${element.key}) can't be decorated. (swc-project/swc)
- plugin Agent component is unavailable: {} (Hmbown/CodeWhale)
…and 90 more across the corpus — use search.
Honest provenance: generated on 2026-08-22 from AI-assisted analysis of the linked records. See how records are made.