affaan-m/ECC · error · Error

Unsafe state-store path

Error message

Unsafe state-store path '${currentPath}': an intermediate component is not a directory

What it means

The state-store creates its database directory (and all intermediate directories) with restrictive permissions (mode 0700). Before creating each segment it lstats the path and rejects any component that exists but is not a directory (e.g. a regular file, FIFO, device node), with an exception only for macOS's root-level /var, /tmp, /etc symlinks to /private/*. This is a path-safety guard: writing a database through a path that traverses a non-directory would fail unpredictably or could be an attacker-planted structure, so the library fails fast with an explicit error naming the offending component.

Solutions

  1. Inspect the path named in the error (ls -la) and remove or rename the offending non-directory component, e.g. mv /home/me/.claude /home/me/.claude.bak then mkdir -p /home/me/.claude
  2. Verify you passed the database FILE path as dbPath (not a directory path, and not dirname of the db), letting resolveStateStorePath place it under ~/.claude/ecc/state.db
  3. If a mount/volume shadowed the directory, unmount or fix the mount so the component is a real directory
  4. If HOME points somewhere unexpected (CI, containers), set HOME or pass options.homeDir so the default path resolves to a writable tree of real directories

Example fix

// before: ~/.claude exists as a regular file, crashes
createStateStore({});
// after: detect and move the file aside before opening the store
import fs from 'fs';
if (fs.existsSync(homeClaude) && fs.lstatSync(homeClaude).isFile()) {
  fs.renameSync(homeClaude, homeClaude + '.backup');
}
const store = await createStateStore({ homeDir: process.env.HOME });
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'fs';
import path from 'path';

export function assertDirectoryChainUsable(dirPath) {
  let current = path.resolve(dirPath);
  const parts = current.split(path.sep).filter(Boolean);
  let acc = path.parse(current).root;
  for (const part of parts) {
    acc = path.join(acc, part);
    const st = fs.lstatSync(acc, { throwIfNoEntry: false });
    if (st && !st.isDirectory() && !st.isSymbolicLink()) {
      throw new Error(`Cannot use state store: '${acc}' exists and is not a directory`);
    }
  }
}

// before creating the store:
assertDirectoryChainUsable(path.dirname(dbPath));

Type guard

function isDirectory(p) {
  const st = fs.lstatSync(p, { throwIfNoEntry: false });
  return st !== undefined && st.isDirectory();
}

Try / catch

try {
  const store = await createStateStore({ dbPath });
} catch (error) {
  if (error.message.includes('intermediate component is not a directory')) {
    const offender = error.message.match(/'([^']+)'/)[1];
    fs.renameSync(offender, offender + '.not-a-directory.bak');
    // retry once
  } else {
    throw error;
  }
}

Prevention

When it happens

Trigger: Calling createStateStore({ dbPath }) or any write that calls ensurePrivateDirectory(dirname(dbPath)) when some intermediate component of the resolved directory path already exists as a non-directory: e.g. dbPath '/home/me/.claude/ecc/state.db' where /home/me/.claude is a regular file (or the directory itself is a file such as '/home/me/.claude'), so the loop hits that segment with stats.isFile() true.

Common situations: A dotfile like ~/.claude exists as a plain file (some tools write ~/.claude as a config file), a leftover state.db file is reused as the directory path (dbPath accidentally passed as the directory), mount points or Docker volume mounts replaced a directory with a file, or a package/tool created a file at a path later expected to be a directory.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/4e415169459e485f. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/state-store/index.js:95

  for (const segment of segments) {
    currentPath = path.join(currentPath, segment);
    let stats = lstatIfPresent(currentPath);
    assertNotSymlink(currentPath, stats);

    if (!stats) {
      try {
        fs.mkdirSync(currentPath, { mode: PRIVATE_DIRECTORY_MODE });
      } catch (error) {
        if (!error || error.code !== 'EEXIST') {
          throw error;
        }
      }
      stats = fs.lstatSync(currentPath);
      assertNotSymlink(currentPath, stats);
    }

    if (!stats.isDirectory() && !isAllowedPlatformSymlink(currentPath, stats)) {
      throw stateStorePathError(currentPath, 'an intermediate component is not a directory');
    }
  }

  return absolutePath;
}

function assertSafeDatabaseFile(dbPath) {
  const stats = lstatIfPresent(dbPath);
  assertNotSymlink(dbPath, stats);
  if (stats && !stats.isFile()) {
    throw stateStorePathError(dbPath, 'database path is not a regular file');
  }
  return stats;
}

function readDatabaseFile(dbPath) {
  assertSafeDatabaseFile(dbPath);
  const noFollow = fs.constants.O_NOFOLLOW || 0;

View on GitHub (pinned to 8321021c54)