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

  1. 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).
  2. 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.
  3. If you need a custom location, point RUFLO_AI_BUDGET_DIR at a real (non-symlinked-file) directory on local disk.
  4. 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

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-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/30eaa69b6dcaa8c7. Report an issue: GitHub.