ruvnet/ruflo · error · Error
Repo-supervisor file is a symlink (refusing): ${path}
Error message
Repo-supervisor file is a symlink (refusing): ${path} What it means
Thrown by assertNotSymlink() (Invariant 9, issue #2661) whenever a repo-supervisor registry file — ~/.claude-flow/supervisors/<repositoryId>.json (or under RUFLO_AI_BUDGET_DIR) — is a symbolic link. The supervisor registry elects one daemon per repository, so a symlinked record could redirect reads/writes outside the registry directory; the module refuses to touch it. ENOENT is allowed (no file yet), any other lstat error propagates.
Source
Thrown at v3/@claude-flow/cli/src/services/repo-supervisor.ts:72
function delay(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
/** Invariant 9 (#2661): registry files must never be symlinks. */
function assertNotSymlink(path: string): void {
try {
const st = fs.lstatSync(path);
if (st.isSymbolicLink()) {
throw new Error(`Repo-supervisor file is a symlink (refusing): ${path}`);
}
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return;
throw e;
}
}
export class RepoSupervisorRegistry {
private readonly dir: string;
constructor(options?: { baseDir?: string }) {
this.dir = options?.baseDir
?? process.env.RUFLO_AI_BUDGET_DIR
?? join(homedir(), '.claude-flow');
}
private fileFor(repositoryId: string): string {
return join(this.dir, 'supervisors', `${repositoryId}.json`);View on GitHub (pinned to fa13ee4ad6)
Solutions
- Inspect the path in the error with ls -l and readlink; decide whether the link is benign (dotfile sync) or unexpected (investigate who created it before deleting anything).
- Replace the symlink with the real file/directory: copy the target's contents to a real ~/.claude-flow and remove the link — the guard checks the file itself, so also ensure the record file is a regular file.
- If you need a custom location, point RUFLO_AI_BUDGET_DIR at a real (non-symlinked-file) directory on local disk.
- If the link is unexplained, treat it as a security signal: audit the machine/worktrees for the same pattern before re-running the daemon.
Example fix
# before: ~/.claude-flow is a symlink, record resolves through it $ ls -l ~/.claude-flow/supervisors/9f3a….json lrwxrwxrwx … 9f3a….json -> /shared/state/9f3a….json # daemon throws: Repo-supervisor file is a symlink (refusing): … # after: materialize real files, drop the link $ mkdir -p ~/.claude-flow-real && cp -L -r ~/.claude-flow/. ~/.claude-flow-real/ $ rm ~/.claude-flow && mv ~/.claude-flow-real ~/.claude-flow
Defensive patterns
Strategy: validation
Validate before calling
import * as fs from 'fs';
import { join } from 'path';
function registryFileIsSafe(baseDir: string, repositoryId: string): boolean {
const file = join(baseDir, 'supervisors', `${repositoryId}.json`);
try { return !fs.lstatSync(file).isSymbolicLink(); }
catch (e) { return (e as NodeJS.ErrnoException).code === 'ENOENT'; } // absent is fine
}
if (!registryFileIsSafe(process.env.RUFLO_AI_BUDGET_DIR ?? join(homedir(), '.claude-flow'), repoId)) {
throw new Error('Supervisor registry file is a symlink — refusing to start (security)');
} Try / catch
try {
return await registry.tryAcquireSupervision(worktreeRoot);
} catch (e) {
if (e instanceof Error && e.message.includes('is a symlink (refusing)')) {
// Stop and investigate: who created the link? Copy target contents to a real
// file/dir, remove the link, then retry on the next daemon tick.
logger.error('Security invariant tripped — refusing supervisor ops until registry is a real file:', e.message);
return { isSupervisor: false, record: null };
}
throw e;
} Prevention
- Never symlink files inside ~/.claude-flow — dotfile managers should manage configs, not the runtime registry directory's files.
- If you relocate the registry, point RUFLO_AI_BUDGET_DIR at a real directory on local disk.
- Audit after restores/migrations: ls -la ~/.claude-flow/supervisors/ should show regular files (-), not l.
- Treat this error as a tamper signal, not a nuisance: identify the link's origin before deleting anything.
When it happens
Trigger: Someone replaced the supervisor JSON (or its path resolves to a symlink) — e.g. a dotfiles manager symlinking ~/.claude-flow to a synced folder, RUFLO_AI_BUDGET_DIR pointing at a symlinked path where the record itself was linked, or an attacker/copy script creating links between worktree registries to share election state.
Common situations: Users syncing ~/.claude-flow across machines with stow/symlink-based dotfile managers; teams sharing a network home where someone linked the registry file to a shared copy; a restore-from-backup that recreated files as links; genuinely hostile pre-creation of symlinks before a privileged write (the attack this guard exists for).
Related errors
- AI job registry is a symlink (refusing): ${path}
- AI budget file is a symlink (refusing): ${path}
- Workspace lease file is a symlink (refusing): ${path}
- Path traversal blocked: ${realResolved}
- Path traversal blocked: ${resolved}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/30eaa69b6dcaa8c7.
Report an issue: GitHub.