dmtrKovalenko/fff · error · Error
Could not read pi-fff config at
Error message
Could not read pi-fff config at ${configPath}: ${errorMessage(error)} What it means
pi-fff's loadConfig() reads the user's JSON config file synchronously and treats only ENOENT as 'no config'. Any other read failure (permissions, EISDIR, I/O errors) is rethrown wrapped in this message with the path and underlying error text.
Solutions
- Check the file at the printed path: ensure it is a regular file, not a directory (`file <path>`; if a dir, remove it and create the JSON file).
- Fix permissions: `chmod u+r <path>` and ensure the process user can read it (check ownership with ls -l).
- If the config is broken/unneeded, move it aside and let pi-fff recreate defaults.
- Read the wrapped underlying error (EACCES vs EISDIR vs ELOOP) to target the exact filesystem problem.
Example fix
// before
$ mkdir -p ~/.config/pi-fff/config.json # wrong: creates a directory
// after
rm -rf ~/.config/pi-fff/config.json
printf '{}' > ~/.config/pi-fff/config.json && chmod 600 ~/.config/pi-fff/config.json Defensive patterns
Strategy: try-catch
Validate before calling
import { statSync, accessSync, constants } from 'fs';
const st = statSync(configPath, { throwIfNoEntry: false });
if (st?.isDirectory()) throw new Error(`${configPath} is a directory, expected a JSON file`);
if (st) accessSync(configPath, constants.R_OK); Try / catch
try {
const cfg = loadConfig();
} catch (e) {
if (String(e.message).startsWith('Could not read pi-fff config')) {
// move broken config aside and continue with defaults
renameSync(configPath, configPath + '.broken');
return {};
}
throw e;
} Prevention
- Never create a directory where the config file is expected.
- Ensure the process user owns/can read the config file (chmod 600).
- Validate config JSON after edits to catch malformed setups early.
- Check the wrapped errno (EACCES/EISDIR/ELOOP) before deeper debugging.
When it happens
Trigger: Calling config()/loadConfig() when the config file at configPath exists but cannot be read: it is a directory instead of a file, the process lacks read permission, a symlink is broken in an unexpected way (ELOOP), or the filesystem errors during read.
Common situations: Config path pointing at a directory (someone created ~/.config/pi-fff/config.json/ as a folder), files owned by another user after switching run users (root vs user), NFS/permissions issues in containers, or SELinux/apparmor denials.
Understand the failure class
Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.
Related errors
AI-assisted analysis of dmtrKovalenko/fff@7f8537e70f (2026-09-10).
Data as JSON: /api/errors/e7b1551739add1c4.
Report an issue: GitHub.
Appendix: source
Thrown at packages/pi-fff/src/config.ts:40
"$schema",
"mode",
"frecencyDbPath",
"historyDbPath",
"enableFsRootScanning",
"enableHomeDirScanning",
"warnOnHomeDirScan",
"followSymlinks",
]);
export function loadConfig(agentDir = piDataDir()): FffConfig {
const configPath = join(agentDir, CONFIG_FILE_NAME);
let contents: string;
try {
contents = readFileSync(configPath, "utf8");
} catch (error: unknown) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
throw new Error(
`Could not read pi-fff config at ${configPath}: ${errorMessage(error)}`,
);
}
let parsed: unknown;
try {
parsed = JSON.parse(contents);
} catch (error: unknown) {
throw invalidConfig(configPath, `not valid JSON (${errorMessage(error)})`);
}
if (!isRecord(parsed)) {
throw invalidConfig(configPath, "expected a JSON object");
}
for (const key of Object.keys(parsed)) {
if (!CONFIG_KEYS.has(key as keyof FffConfig)) {
throw invalidConfig(configPath, `unknown option "${key}"`);View on GitHub (pinned to 7f8537e70f)