google-gemini/gemini-cli · error

Circular symlink detected

Error message

Circular symlink detected

What it means

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.

Solutions

  1. Inspect the path chain with 'namei -l <path>' or 'readlink -f <path>' and delete/replace the cyclical symlink.
  2. Recreate the affected link as a plain directory or one pointing to a real, acyclic target.
  3. Restore the .gemini directory from a known-good state (e.g. remove and let the CLI reinitialize).

Example fix

// before
$ ln -s ~/config ~/.gemini   # where ~/config -> ~/.gemini (loop)
// after
$ rm ~/.gemini && mkdir ~/.gemini
Defensive patterns

Strategy: try-catch

Validate before calling

try {
  fs.realpathSync(p);
} catch {
  throw new Error(`path ${p} has an unresolvable (possibly circular) symlink`);
}

Try / catch

try {
  const real = safeResolveToRealPath(p);
} catch (e) {
  if (e.message === 'Circular symlink detected') {
    // surface a fix hint: run namei -l <p> and remove the loop
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of google-gemini/gemini-cli@6a466a7e2f (2026-09-16). Data as JSON: /api/errors/8df21d4b58a4f8f8. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/utils/sandboxUtils.ts:115

    return true;
  }
  return false;
}

/**
 * Resolves a path to its real path, falling back to path.resolve if it does not exist (ENOENT).
 * Rethrows unrecoverable errors so callers can fail closed.
 */
function safeResolveToRealPath(targetPath: string): string {
  let current = path.resolve(targetPath);
  const parts: string[] = [];
  const visited = new Set<string>();

  while (current && current !== path.dirname(current)) {
    const visitKey =
      os.platform() === 'win32' ? current.toLowerCase() : current;
    if (visited.has(visitKey)) {
      throw new Error('Circular symlink detected');
    }
    visited.add(visitKey);

    try {
      const real = resolveToRealPath(current);
      return path.resolve(real, ...parts.slice().reverse());
    } catch (err: unknown) {
      if (isRecord(err) && err['code'] === 'ENOENT') {
        try {
          const stat = fs.lstatSync(current);
          if (stat?.isSymbolicLink?.()) {
            const target = fs.readlinkSync(current);
            current = path.resolve(path.dirname(current), target);
            continue;
          }
        } catch (lstatErr: unknown) {
          if (!isRecord(lstatErr) || lstatErr['code'] !== 'ENOENT') {
            throw lstatErr;

View on GitHub (pinned to 6a466a7e2f)