{"record":{"id":"45dcd8e2a7646573","repo":"ruvnet/ruflo","slug":"ai-budget-file-is-a-symlink-refusing-path","errorCode":null,"errorMessage":"AI budget file is a symlink (refusing): ${path}","messagePattern":"AI budget file is a symlink \\(refusing\\): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/services/global-ai-budget.ts","lineNumber":126,"sourceCode":"  const n = Number.parseInt(raw, 10);\n  return Number.isFinite(n) && n >= 0 ? n : undefined;\n}\n\nfunction isProcessAlive(pid: number): boolean {\n  try {\n    process.kill(pid, 0);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/** Invariant 9: registry files must never be symlinks. */\nfunction assertNotSymlink(path: string): void {\n  try {\n    const st = fs.lstatSync(path);\n    if (st.isSymbolicLink()) {\n      throw new Error(`AI budget file is a symlink (refusing): ${path}`);\n    }\n  } catch (e) {\n    if ((e as NodeJS.ErrnoException).code === 'ENOENT') return;\n    throw e;\n  }\n}\n\nfunction delay(ms: number): Promise<void> {\n  return new Promise((r) => setTimeout(r, ms));\n}\n\nexport class GlobalAiBudget {\n  private readonly dir: string;\n  private readonly ledgerFile: string;\n  private readonly lockFile: string;\n  private readonly receiptsFile: string;\n  private readonly limits: AiBudgetLimits;\n","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/cli/src/services/global-ai-budget.ts#L108-L144","documentation":"Thrown by assertNotSymlink() inside GlobalAiBudget when the AI budget ledger file (~/.claude-flow/ai-budget.json) or receipts file (ai-budget-receipts.jsonl) is a symbolic link. This enforces 'Invariant 9: registry files must never be symlinks.' The AI budget fuse fails closed — writing through a symlink would let an attacker redirect or duplicate the launch accounting, defeating the concurrency/budget limits.","triggerScenarios":"Calling GlobalAiBudget.reserve()/release()/getStatus() when ai-budget.json or ai-budget-receipts.jsonl (in the configured RUFLO_AI_BUDGET_DIR or ~/.claude-flow) is a symlink. assertNotSymlink runs on every readLedger() and writeLedger().","commonSituations":"A shared-home setup where ~/.claude-flow is symlinked to a network share; a dotfile-manager that symlinks config files; a tampering attempt to point the ledger at /dev/null or a duplicate; an accidental ln -s during setup.","solutions":["Replace the symlink with a real file: rm <symlink> && touch <file>.","Audit ~/.claude-flow (or RUFLO_AI_BUDGET_DIR) for symlinks.","Ensure the budget directory is a real directory, not a symlink to one.","If you genuinely need a shared budget dir, mount it as a real filesystem rather than symlinking individual files."],"exampleFix":"# before: ~/.claude-flow/ai-budget.json -> /shared/ai-budget.json\nrm ~/.claude-flow/ai-budget.json\n# recreate as real file (the code re-initializes it)","handlingStrategy":"validation","validationCode":"function assertBudgetDirClean(dir: string): void {\n  for (const f of ['ai-budget.json', 'ai-budget-receipts.jsonl']) {\n    const full = path.join(dir, f);\n    try {\n      if (fs.lstatSync(full).isSymbolicLink()) {\n        throw new Error(`security: ${full} is a symlink — replace with a real file`);\n      }\n    } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; }\n  }\n}","typeGuard":"const isRegularBudgetFile = (p: string): boolean => {\n  try { return fs.lstatSync(p).isFile(); } catch { return false; }\n};","tryCatchPattern":"try {\n  await budget.reserve(req);\n} catch (e) {\n  if (e instanceof Error && /AI budget file is a symlink/.test(e.message)) {\n    throw new Error('security: AI budget registry tampered with — refusing to launch');\n  }\n  throw e;\n}","preventionTips":["Keep ~/.claude-flow as a real directory on local storage.","Audit the budget dir for symlinks in CI and on deploy.","Do not let dotfile managers symlink individual budget files."],"tags":["security","filesystem","symlink","budget","hardening"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}