ruvnet/ruflo · critical · Error
Workspace lease file is a symlink (refusing): ${path}
Error message
Workspace lease file is a symlink (refusing): ${path} What it means
WorkspaceLeaseRegistry (Invariant 9, issue #2661) hard-refuses to operate on any registry file that is a symbolic link. assertNotSymlink() lstat()s the lease file before every read/write and throws if st.isSymbolicLink(). This blocks symlink-swap attacks where an attacker replaces the lease registry with a link to redirect writes or read privileged data. ENOENT is allowed (a not-yet-created file is fine); every other lstat error propagates.
Source
Thrown at v3/@claude-flow/cli/src/services/workspace-lease.ts:61
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(`Workspace lease file is a symlink (refusing): ${path}`);
}
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return;
throw e;
}
}
function leaseKey(worktreeRoot: string): string {
return createHash('sha256').update(worktreeRoot).digest('hex').slice(0, 16);
}
export class WorkspaceLeaseRegistry {
private readonly dir: string;
constructor(options?: { baseDir?: string }) {
this.dir = options?.baseDir
?? process.env.RUFLO_AI_BUDGET_DIR // shares the same override as global-ai-budget for test isolation
?? join(homedir(), '.claude-flow');View on GitHub (pinned to fa13ee4ad6)
Solutions
- Remove the symlink and restore the registry path as a real file or directory (ls -l to confirm), then retry the lease operation
- If you moved the state dir, move it physically (mv) instead of leaving a symlink behind
- Audit how the symlink got there — if you did not create it, treat it as a security incident on that worktree
- Keep symlinks out of the workspace/state directories this registry manages
Example fix
# before: registry path is a symlink ls -l .claude-flow/workspace-leases.json # -> .claude-flow/workspace-leases.json -> /etc/sensitive # after: replace with a real file rm .claude-flow/workspace-leases.json touch .claude-flow/workspace-leases.json # registry recreates structured content on next write
Defensive patterns
Strategy: validation
Validate before calling
import * as fs from 'node:fs';
function assertRegistryFileReal(path: string): void {
let st;
try { st = fs.lstatSync(path); } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') return; throw e; }
if (st.isSymbolicLink()) {
throw new Error(`Refusing to operate: ${path} is a symlink — replace it with a real file first`);
}
}
assertRegistryFileReal(registryFilePath); // run before acquiring leases Try / catch
try {
await registry.acquire(worktree, owner);
} catch (e) {
if (/is a symlink \(refusing\)/.test(String(e?.message))) {
// operational hazard, not transient: surface to the user to remove the link manually
throw new Error(`Security invariant violated: ${e.message}. Remove the symlink and investigate how it appeared.`);
}
throw e;
} Prevention
- Never replace claude-flow state/registry files with symlinks — move the directory physically instead
- Exclude claude-flow state dirs from dotfile managers and backup tools that recreate files as links
- Treat an unexpected symlink in the registry location as a possible compromise of that worktree, not as a nuisance error
When it happens
Trigger: A symlink existing at the lease registry path (e.g. .claude-flow registry file in the worktree root) when any lease acquire/release/read runs; dotfile managers or backup tools that replace state files with symlinks; a compromised or misconfigured workspace planting a link before the daemon takes a lease.
Common situations: Developers sharing state dirs via symlinks (moving ~/.claude-flow or the repo state dir to another volume); CI setups that symlink cache/state directories; security tooling testing the invariant; genuinely hostile worktrees where a checkout included a symlink at the registry path.
Related errors
- AI job registry is a symlink (refusing): ${path}
- Repo-supervisor file is a symlink (refusing): ${path}
- localSingleEntryPageRank: sourceIndex ${src} out of range [0
- extracted binary path failed validation: ${validation.errors
- AI budget file is a symlink (refusing): ${path}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/c57b62dfe56bc877.
Report an issue: GitHub.