ruvnet/ruflo · error
AI job registry is a symlink (refusing)
Error message
AI job registry is a symlink (refusing): ${path} What it means
AiJobDedupRegistry enforces 'Invariant 9: registry files must never be symlinks'. Before touching the AI job registry file it lstat()s the path and refuses to proceed if it is a symbolic link. This is a deliberate anti-tampering guard: a symlinked registry lets another user/process swap the file underneath (or point it at a sensitive path), so the registry aborts rather than read or write through the link. ENOENT (file does not exist yet) is allowed; every other stat error is rethrown.
Solutions
- Inspect the path from the error message with ls -l <path> and confirm it is a symlink
- Replace the symlink with a real file (cp -L <link> <tmp> && rm <link> && mv <tmp> <path>) or remove it so the registry recreates a regular file
- If sharing across projects/machines was the goal, point options.baseDir at a real directory on the target filesystem instead of symlinking the file
- Do not bypass the check — it is a security invariant; if the symlink was unexpected, investigate who created it
Example fix
# before ~/.claude-flow/ai-jobs/registry.json -> /shared/ai-jobs.json (symlink) # constructing AiJobDedupRegistry throws # after mkdir -p ~/.claude-flow/ai-jobs cp -L /shared/ai-jobs.json ~/.claude-flow/ai-jobs/registry.json.tmp 2>/dev/null || true rm ~/.claude-flow/ai-jobs/registry.json mv ~/.claude-flow/ai-jobs/registry.json.tmp ~/.claude-flow/ai-jobs/registry.json
Defensive patterns
Strategy: validation
Validate before calling
const registryPath = path.join(baseDir, 'ai-jobs', 'registry.json'); // path used by AiJobDedupRegistry
const st = fs.lstatSync(registryPath); // throws ENOENT if absent — that case is fine
if (st.isSymbolicLink()) {
throw new Error(`${registryPath} is a symlink; replace it with a real file before enabling AI job dedup`);
} Type guard
const isRealFile = (p: string): boolean => {
try { return fs.lstatSync(p).isFile(); } catch (e) { return (e as NodeJS.ErrnoException).code === 'ENOENT'; }
}; Try / catch
try {
registry = new AiJobDedupRegistry({ baseDir });
} catch (e) {
if (e instanceof Error && e.message.includes('is a symlink')) {
// surface to the operator: replace the symlink with a real file; do NOT auto-delete — it may be an attack indicator
} else throw e;
} Prevention
- Exclude registry/data files from dotfiles managers and sync tools; keep them real files on local disk
- In setup scripts, assert !fs.lstatSync(p).isSymbolicLink() before first use
- If this fires unexpectedly, treat it as a security signal and audit who created the link
When it happens
Trigger: Constructing AiJobDedupRegistry (or any flow that instantiates it) when the registry file under its baseDir (options.baseDir or the default) is a symlink — e.g. ~/.claude-flow/ai-jobs or a nested registry.json managed by GNU stow, dotfiles repos, syncthing/cloud-sync, or a manual 'ln -s' to shared storage.
Common situations: Home directory managed with dotfiles tooling that symlinks config/data paths, multi-agent setups sharing one registry across machines via a symlinked network folder, or a security-conscious environment where the guard is tripping on purpose because something replaced the file.
Related errors
- Repo-supervisor file is a symlink (refusing)
- AI budget file is a symlink (refusing)
- Path traversal blocked
- Path traversal blocked
- refusing symlink
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/dd697ec3705e15d9.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/services/ai-job-dedup.ts:69
}
/** Stable hash of an arbitrary config object (key-sorted JSON). */
export function hashWorkerConfig(config: unknown): string {
const canonical = JSON.stringify(config, (_k, v) => {
if (v && typeof v === 'object' && !Array.isArray(v)) {
return Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)));
}
return v;
});
return createHash('sha256').update(canonical ?? 'null').digest('hex');
}
/** Invariant 9: registry files must never be symlinks. */
function assertNotSymlink(path: string): void {
try {
const st = fs.lstatSync(path);
if (st.isSymbolicLink()) {
throw new Error(`AI job registry is a symlink (refusing): ${path}`);
}
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return;
throw e;
}
}
export class AiJobDedupRegistry {
private readonly dir: string;
private readonly file: string;
constructor(options?: { baseDir?: string }) {
this.dir = options?.baseDir
?? process.env.RUFLO_AI_BUDGET_DIR
?? join(homedir(), '.claude-flow');
this.file = join(this.dir, 'ai-jobs.json');
}
View on GitHub (pinned to fa13ee4ad6)