affaan-m/ECC · error

Unable to read at

Error message

Unable to read ${label} at ${filePath}: ${error.message}

What it means

readJsonObject() in scripts/lib/hooks-config.js reads a JSON file from disk (labelled, e.g. settings.json or the hooks metadata sidecar). If fs.readFileSync throws — file missing, permission denied, path is a directory — it rethrows as 'Unable to read <label> at <filePath>: <reason>'. This gives callers a consistent error naming the file and the underlying OS reason.

Solutions

  1. Create the file at <filePath> (e.g. run the setup step that initializes hooks config) or point the tool at an existing file.
  2. Fix file permissions so the current user can read it (chmod/chown).
  3. Verify the path is a file, not a directory, and that you are running from the expected project root.
  4. Read error.message in the thrown error for the exact OS reason (ENOENT vs EACCES vs EISDIR).

Example fix

// before
const cfg = readHooksConfig('.claude/settings.json'); // ENOENT if missing
// after
const fs = require('fs');
const path = '.claude/settings.json';
if (!fs.existsSync(path)) fs.writeFileSync(path, '{}');
const cfg = readHooksConfig(path);
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
function canReadFile(p) {
  try { return fs.statSync(p).isFile() && fs.accessSync(p, fs.constants.R_OK) === undefined; }
  catch { return false; }
}
if (!canReadFile('.claude/settings.json')) {
  fs.writeFileSync('.claude/settings.json', '{}'); // create default before reading
}

Type guard

function isReadableFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }

Try / catch

try {
  const cfg = readHooksConfig(path);
} catch (err) {
  if (String(err.message).startsWith('Unable to read')) {
    console.error(`Cannot read ${path}: ${err.message}. Creating default.`);
    fs.writeFileSync(path, '{}');
  } else throw err;
}

Prevention

When it happens

Trigger: Calling hooksConfig(), readHooksConfig(), or addJsonMergeOperation() where the hooks/settings JSON path does not exist, is unreadable due to permissions, is a directory, or sits on an unmounted volume.

Common situations: Running ECC hooks tooling before the project's settings.json exists; wrong working directory so relative paths resolve elsewhere; restrictive file permissions after cloning as another user; ENOENT on the metadata sidecar during first-time setup.

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 affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/685c2bc6dfe956ac. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/hooks-config.js:32

 * so every sidecar entry also carries a fingerprint of the matcher entry it
 * describes. A mismatch means the two files drifted apart.
 */

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

const HOOKS_FILENAME = 'hooks.json';
const METADATA_FILENAME = 'hooks.metadata.json';
const FINGERPRINT_LENGTH = 12;
const FINGERPRINT_PATTERN = /^[0-9a-f]{12}$/;

function readJsonObject(filePath, label) {
  let raw;
  try {
    raw = fs.readFileSync(filePath, 'utf8');
  } catch (error) {
    throw new Error(`Unable to read ${label} at ${filePath}: ${error.message}`);
  }

  let parsed;
  try {
    parsed = JSON.parse(raw);
  } catch (error) {
    throw new Error(`Invalid JSON in ${label} at ${filePath}: ${error.message}`);
  }

  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error(`Invalid ${label} at ${filePath}: expected a JSON object`);
  }

  return parsed;
}

function metadataPathFor(hooksPath) {
  return path.join(path.dirname(hooksPath), METADATA_FILENAME);

View on GitHub (pinned to 8321021c54)