{"record":{"id":"8df21d4b58a4f8f8","repo":"google-gemini/gemini-cli","slug":"circular-symlink-detected","errorCode":null,"errorMessage":"Circular symlink detected","messagePattern":"Circular symlink detected","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/utils/sandboxUtils.ts","lineNumber":115,"sourceCode":"    return true;\n  }\n  return false;\n}\n\n/**\n * Resolves a path to its real path, falling back to path.resolve if it does not exist (ENOENT).\n * Rethrows unrecoverable errors so callers can fail closed.\n */\nfunction safeResolveToRealPath(targetPath: string): string {\n  let current = path.resolve(targetPath);\n  const parts: string[] = [];\n  const visited = new Set<string>();\n\n  while (current && current !== path.dirname(current)) {\n    const visitKey =\n      os.platform() === 'win32' ? current.toLowerCase() : current;\n    if (visited.has(visitKey)) {\n      throw new Error('Circular symlink detected');\n    }\n    visited.add(visitKey);\n\n    try {\n      const real = resolveToRealPath(current);\n      return path.resolve(real, ...parts.slice().reverse());\n    } catch (err: unknown) {\n      if (isRecord(err) && err['code'] === 'ENOENT') {\n        try {\n          const stat = fs.lstatSync(current);\n          if (stat?.isSymbolicLink?.()) {\n            const target = fs.readlinkSync(current);\n            current = path.resolve(path.dirname(current), target);\n            continue;\n          }\n        } catch (lstatErr: unknown) {\n          if (!isRecord(lstatErr) || lstatErr['code'] !== 'ENOENT') {\n            throw lstatErr;","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/6a466a7e2fe2b1255752c1e74f69b31f0216084d/packages/cli/src/utils/sandboxUtils.ts#L97-L133","documentation":"safeResolveToRealPath walks up the path chain (toward the filesystem root), remembering visited components in a Set keyed by normalized (lowercased on Windows) paths. If the walk revisits a component, the symlink structure is a loop and resolution would never terminate, so it throws. This protects path resolution from malicious or broken circular symlinks.","triggerScenarios":"safeResolveToRealPath (used by resolvedPath/home/normalized/geminiDirOnHost) walking a path whose ancestor chain contains a symlink cycle, e.g. a symlink pointing to its own parent or a symlinked directory that loops back.","commonSituations":"A broken dotfiles setup where ~/.gemini or a config directory is a symlink into itself; restoring backups that recreated recursive symlinks; container/host shared paths with cyclical links.","solutions":["Inspect the path chain with 'namei -l <path>' or 'readlink -f <path>' and delete/replace the cyclical symlink.","Recreate the affected link as a plain directory or one pointing to a real, acyclic target.","Restore the .gemini directory from a known-good state (e.g. remove and let the CLI reinitialize)."],"exampleFix":"// before\n$ ln -s ~/config ~/.gemini   # where ~/config -> ~/.gemini (loop)\n// after\n$ rm ~/.gemini && mkdir ~/.gemini","handlingStrategy":"try-catch","validationCode":"try {\n  fs.realpathSync(p);\n} catch {\n  throw new Error(`path ${p} has an unresolvable (possibly circular) symlink`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const real = safeResolveToRealPath(p);\n} catch (e) {\n  if (e.message === 'Circular symlink detected') {\n    // surface a fix hint: run namei -l <p> and remove the loop\n  }\n}","preventionTips":["Audit dotfiles that symlink ~/.gemini; never create links that point into their own subtree.","Use readlink -f / namei -l to verify paths after restoring from backups.","Prefer copying directories over symlinking for CLI config paths."],"tags":["filesystem","symlink","sandbox"],"backgroundTag":"symlink-loop","analyzedSha":"6a466a7e2fe2b1255752c1e74f69b31f0216084d","analyzedAt":"2026-09-16T18:14:43.978Z","contentChangedAt":"2026-09-16T18:14:43.978Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}