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
- Replace the symlink with a real directory: rm the link, mkdir the path, and copy contents back (rsync -a link/ realdir/), then retry.
- Point ECC at the real location via its state-store path configuration instead of symlinking, keeping every component a physical directory.
- On macOS, only the system /var, /tmp, /etc → /private/* aliases are allowed; any user symlink must be removed regardless of target.
- 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
- Never symlink ~/.claude or any parent of state.db (dotfile managers: link files, not the directory).
- Configure ECC's state-store path to point at a real directory instead of relocating the DB with ln -s.
- After dotfile/bootstrap scripts run, lstat -check that ~/.claude is a real directory.
- On macOS, expect only /var, /tmp, /etc system aliases to be accepted.
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
- Refusing to manage legacy sync path through symlinked…
- artifact path must be canonical and absolute
- gate.variant_invalid
- Invalid ECC repo root: missing package.json at
- Nasiko executable must be a regular file, not a symlink.
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)