{"record":{"id":"411fc047211b5b40","repo":"jdx/mise","slug":"existing-symbolic-link-conflicts-with-adoption-e","errorCode":null,"errorMessage":"existing symbolic link conflicts with adoption: {entry}","messagePattern":"existing symbolic link conflicts with adoption: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/remote_repository.rs","lineNumber":329,"sourceCode":"                    &format!(\"refs/remotes/origin/{branch}\"),\n                    revision,\n                ],\n            )?;\n        } else if dry_run {\n            miseprintln!(\n                \"Would keep the existing checkout of {origin} at {shown}; --update fast-forwards it\"\n            );\n        }\n        return Ok(destination.to_path_buf());\n    }\n    let nonempty = destination.exists() && destination.read_dir()?.next().is_some();\n    if nonempty {\n        for entry in entries.split('\\0').filter(|s| !s.is_empty()) {\n            let mut ancestor = destination.to_path_buf();\n            for component in Path::new(entry).components() {\n                ancestor.push(component);\n                if ancestor.is_symlink() {\n                    bail!(\"existing symbolic link conflicts with adoption: {entry}\");\n                }\n            }\n            let existing = destination.join(entry);\n            if existing.exists()\n                && (checkout.join(entry).is_symlink()\n                    || !existing.is_file()\n                    || std::fs::read(&existing)? != std::fs::read(checkout.join(entry))?)\n            {\n                bail!(\"existing file conflicts with adoption: {entry}\");\n            }\n        }\n        if dry_run {\n            let new_files = entries\n                .split('\\0')\n                .filter(|s| !s.is_empty())\n                .filter(|entry| !destination.join(entry).exists())\n                .count();\n            miseprintln!(","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/remote_repository.rs#L311-L347","documentation":"When adopting a non-empty destination directory as the global-configuration repository, mise walks every path component of every file in the transferred tree and rejects the adoption if any ancestor of an entry in the destination is a symbolic link. Symlinked ancestors could redirect file writes outside the destination, so adoption is aborted to keep the operation safe and contained.","triggerScenarios":"install/install_source/preview_source (install_at) into a non-empty directory without `.git`, where any parent directory of a tracked entry (e.g. `<dest>/config` when adopting file `config/settings.toml`) is a symlink on disk.","commonSituations":"Users symlink directories like `~/.config/mise/tasks` to a dotfiles-managed location; a dotfiles manager (GNU Stow, chezmoi) created symlinks inside the destination; `settings` or subdirectories were symlinked for sharing across machines.","solutions":["Remove the conflicting symlink(s): `find <destination> -type l` to list them, then `unlink <symlink>` or `rm <symlink>`.","Replace symlinks with real directories, copying content back if needed: `cp -rL <link> <tmp> && rm <link> && mv <tmp> <dir>`.","If a dotfiles manager owns the links, exclude the global-config directory from it or restructure the layout.","Re-run the install after no ancestor of any tracked entry is a symlink."],"exampleFix":"// before\n$ ls -l ~/.config/mise/tasks\ntasks -> ~/dotfiles/mise-tasks\n\n// after\n$ unlink ~/.config/mise/tasks\n$ cp -r ~/dotfiles/mise-tasks ~/.config/mise/tasks","handlingStrategy":"validation","validationCode":"const fs = require('fs'), path = require('path');\nfor (const entry of trackedFiles) {\n  let dir = dest;\n  for (const part of entry.split('/').slice(0, -1)) {\n    dir = path.join(dir, part);\n    if (fs.lstatSync(dir).isSymbolicLink()) throw new Error('symlinked ancestor blocks adoption: ' + dir);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await installSource(bundle, origin);\n} catch (e) {\n  if (/symbolic link conflicts/.test(String(e.cause ?? e))) {\n    console.error('replace symlinked directories inside the destination with real directories');\n  }\n  throw e;\n}","preventionTips":["Do not symlink subdirectories inside the global-config destination; keep real files there","If using GNU Stow/chezmoi, exclude the global-config directory from symlink farm management","Run `find <dest> -type l` before adopting a directory"],"tags":["git","symlink","adoption","path-safety"],"backgroundTag":"path-traversal-blocked","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}