Hmbown/CodeWhale · error · Error
file is not a regular single-link file
Error message
file is not a regular single-link file
What it means
readBoundedFile performs defensive reads of key/envelope files: it first lstat()s the path and requires a regular file with exactly one hard link, throwing this error otherwise. This prevents following symlinks, FIFOs, device nodes, or hardlinked files that could be swapped or point elsewhere.
Solutions
- Replace the symlink with a real copy of the file (cp -L) and point at the copy
- Check with `ls -l` and `stat` for link count and type; remove extra hard links
- Ensure the file is created as a regular file, not a FIFO or socket
- If symlinking is intentional for your workflow, read the target yourself and pass the bytes/env var instead
Example fix
// before CODEWHALE_FACTS_SIGNING_KEY_FILE=~/.secrets/key.pem # symlink // after cp -L ~/.secrets/key.pem ./key.pem && stat -c '%h %F' ./key.pem CODEWHALE_FACTS_SIGNING_KEY_FILE=./key.pem
Defensive patterns
Strategy: validation
Validate before calling
import { lstatSync, constants as fsC } from 'node:fs';
const st = lstatSync(path);
if (!st.isFile() || st.nlink !== 1) throw new Error(`${path} must be a regular single-link file`); Type guard
const isRegularSingleLink = (path) => { try { const s = lstatSync(path); return s.isFile() && s.nlink === 1; } catch { return false; } }; Try / catch
try { pem = readBoundedFile(keyFile, 16 * 1024); } catch (e) { if (e.message === 'file is not a regular single-link file') { console.error(`Replace symlink/FIFO/hardlink at ${keyFile} with a real file`); process.exit(2); } throw e; } Prevention
- Use `cp -L` to materialize symlinked secrets into real files
- Check `stat -c '%h %F'` on key files before pointing the script at them
- Keep secrets in a dedicated directory without dotfiles symlink farms
- Never point the script at pipes or process substitution targets
When it happens
Trigger: Calling readBoundedFile(path) where the path is a symlink (lstat isFile() false), a FIFO/socket/device, or a regular file with nlink > 1 (hard-linked).
Common situations: Pointing CODEWHALE_FACTS_SIGNING_KEY_FILE at a symlink into a secrets manager or dotfiles symlink farm; a temp file hard-linked by a backup tool; the key file being created by a pipe.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Automation lock must not be a reparse point
- CodewhalePet/1
- could not securely open
- external credential path must name a non-reparse regular…
- is a reparse point, not a workspace-owned file
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/74cfc6242d57a029.
Report an issue: GitHub.
Appendix: source
Thrown at web/scripts/facts-publish.mjs:344
function parseArgs(argv) {
const positional = [];
const flags = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg.startsWith("--")) {
const key = arg.slice(2);
const next = argv[i + 1];
if (next === undefined || next.startsWith("--")) flags[key] = true;
else { flags[key] = next; i += 1; }
} else positional.push(arg);
}
return { positional, flags };
}
/** Bounded, regular, single-link file reads; no symlink or FIFO following. */
export function readBoundedFile(path, maxBytes = MAX_ENVELOPE_BYTES) {
const before = lstatSync(path);
if (!before.isFile() || before.nlink !== 1) throw new Error("file is not a regular single-link file");
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
try {
const stat = fstatSync(fd);
if (!stat.isFile() || stat.nlink !== 1 || stat.size > maxBytes || stat.ino !== before.ino || stat.dev !== before.dev) throw new Error("file is not a bounded regular single-link file");
const bytes = Buffer.alloc(maxBytes + 1);
let size = 0;
while (size <= maxBytes) {
const count = readSync(fd, bytes, size, maxBytes + 1 - size, null);
if (!count) break;
size += count;
}
if (size > maxBytes) throw new Error("file exceeds size limit");
return bytes.subarray(0, size);
} finally { closeSync(fd); }
}
function loadPrivateKeyFromEnv() {
refuseUnderCi();View on GitHub (pinned to 433685b202)