affaan-m/ECC · error · Error

Unsafe state-store path

Error message

Unsafe state-store path '${targetPath}': a symlink is not allowed

What it means

assertNotSymlink in scripts/lib/state-store/index.js guards the ECC state store (state.db and its parent directories) against symlink-based attacks: it lstats every path component and throws 'Unsafe state-store path ... a symlink is not allowed' when any component is a symlink, unless it is a macOS-owned /var|/tmp|/etc → /private/* alias. The store's DB contents are treated as sensitive, so symlinked paths are rejected outright.

Solutions

  1. Replace the symlink with a real directory: rm the link, mkdir the path, and copy contents back (rsync -a link/ realdir/), then retry.
  2. Point ECC at the real location via its state-store path configuration instead of symlinking, keeping every component a physical directory.
  3. On macOS, only the system /var, /tmp, /etc → /private/* aliases are allowed; any user symlink must be removed regardless of target.
  4. Reconfigure your dotfile manager to manage the contents of ~/.claude rather than the directory itself.

Example fix

// before: symlinked state dir
ln -s ~/Dropbox/claude ~/.claude
// -> Unsafe state-store path '~/.claude': a symlink is not allowed

// after: real directory, sync contents instead
rm ~/.claude
mkdir ~/.claude
rsync -a ~/Dropbox/claude/ ~/.claude/
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
const path = require('path');
function hasNoSymlinkComponents(p) {
  let cur = path.parse(path.resolve(p)).root;
  for (const seg of path.resolve(p).split(path.sep).filter(Boolean)) {
    cur = path.join(cur, seg);
    const st = fs.lstatSync(cur, { throwIfNoEntry: false });
    if (st && st.isSymbolicLink()) return false;
  }
  return true;
}

Type guard

function isRealDirectory(p) {
  try { const st = fs.lstatSync(p); return st.isDirectory() && !st.isSymbolicLink(); }
  catch { return false; }
}

Try / catch

try {
  openStateStore(dbPath);
} catch (e) {
  if (String(e.message).includes('a symlink is not allowed')) {
    console.error('State-store path contains a symlink:', e.message);
    console.error('Replace it with a real directory (see ECC state-store path config).');
  } else throw e;
}

Prevention

When it happens

Trigger: Opening/initializing the state store calls ensurePrivateDirectory(dirPath) (any path component, including ~/.claude, is a symlink) or assertSafeDatabaseFile(dbPath) (the state.db file itself is a symlink), e.g. when ECC_STATE_STORE path or ~/.claude was replaced by a symlink.

Common situations: User symlinked ~/.claude into a Dropbox/ synced folder to share settings; dotfile managers (stow, chezmoi, GNU stow farms) replaced .claude with a link; CI containers bind-mounting a symlinked cache directory; moving the DB with ln -s instead of an env/config path.

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


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

Appendix: source

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

  ]);
  const expectedTarget = allowedTargets.get(targetPath);
  if (!expectedTarget) {
    return false;
  }

  try {
    return fs.realpathSync(targetPath) === expectedTarget;
  } catch (_error) {
    return false;
  }
}

function assertNotSymlink(targetPath, stats) {
  if (stats && stats.isSymbolicLink()) {
    if (isAllowedPlatformSymlink(targetPath, stats)) {
      return;
    }
    throw stateStorePathError(targetPath, 'a symlink is not allowed');
  }
}

function ensurePrivateDirectory(directoryPath) {
  const absolutePath = path.resolve(directoryPath);
  const parsed = path.parse(absolutePath);
  const segments = absolutePath.slice(parsed.root.length).split(path.sep).filter(Boolean);
  let currentPath = parsed.root;

  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) {

View on GitHub (pinned to 8321021c54)