{"record":{"id":"4ff3919399b134db","repo":"warpdotdev/warp","slug":"config-symlink-rejected","errorCode":"config_symlink_rejected","errorMessage":"config_symlink_rejected","messagePattern":"config_symlink_rejected","errorType":"exception","errorClass":"MergeError","httpStatus":null,"severity":"error","filePath":"resources/bundled/skills/tui-migrate-setup/scripts/merge_mcp_config.py","lineNumber":114,"sourceCode":"    if path:\n        current[path[-1]] = value\n    else:\n        document.clear()\n        document.update(value)\n\n\ndef _read_regular_file(path: Path, *, missing_ok: bool) -> tuple[bytes, int | None]:\n    try:\n        metadata = path.lstat()\n    except FileNotFoundError:\n        if missing_ok:\n            return b\"\", None\n        raise MergeError(\"source_config_unavailable\")\n    except OSError as error:\n        raise MergeError(\"config_unavailable\") from error\n\n    if stat.S_ISLNK(metadata.st_mode):\n        raise MergeError(\"config_symlink_rejected\")\n    if not stat.S_ISREG(metadata.st_mode):\n        raise MergeError(\"config_not_regular_file\")\n    if metadata.st_size > MAX_CONFIG_BYTES:\n        raise MergeError(\"config_too_large\")\n\n    flags = os.O_RDONLY\n    if hasattr(os, \"O_NOFOLLOW\"):\n        flags |= os.O_NOFOLLOW\n    try:\n        descriptor = os.open(path, flags)\n        with os.fdopen(descriptor, \"rb\") as file:\n            raw = file.read(MAX_CONFIG_BYTES + 1)\n    except OSError as error:\n        raise MergeError(\"config_unavailable\") from error\n    if len(raw) > MAX_CONFIG_BYTES:\n        raise MergeError(\"config_too_large\")\n    return raw, stat.S_IMODE(metadata.st_mode)\n","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/warpdotdev/warp/blob/e72fd7aacbbb2236d9b3be2aad7e7178fe94b4bc/resources/bundled/skills/tui-migrate-setup/scripts/merge_mcp_config.py#L96-L132","documentation":"The merge refuses symlinked MCP config files outright: if lstat shows S_ISLNK, config_symlink_rejected is raised before any read. The tool later writes the destination atomically with O_NOFOLLOW and 0o600, and reading through a symlink could pull in or replace an unintended target — so links are rejected regardless of where they point.","triggerScenarios":"The file passed as --source or --destination is a symlink — typical of dotfiles managers (stow, chezmoi, install scripts) or a manual ln -s to a shared config across machines.","commonSituations":"Dotfiles-managed machines; multi-account setups sharing one config via symlink; fresh dotfiles clones where the manager symlinks instead of copying.","solutions":["Materialize the file: cp -L <path> <tmp> && mv <tmp> <path>, replacing the symlink with its content","Point the argument directly at the real file the symlink resolves to (check with readlink -f)","Reconfigure the dotfiles tool to copy/template this config — MCP configs contain secrets, so per-machine real files are safer anyway"],"exampleFix":"# before\n~/.claude.json -> ~/dotfiles/claude.json  (symlink)\npython merge_mcp_config.py --source ~/.claude.json ... # error: config_symlink_rejected\n\n# after\nREAL=$(readlink -f ~/.claude.json)\npython merge_mcp_config.py --source \"$REAL\" ...","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nfor p in (Path(args.source), Path(args.destination)):\n    if p.is_symlink():\n        raise SystemExit(f'{p} is a symlink; pass the real file or materialize it first')","typeGuard":null,"tryCatchPattern":"if result.returncode != 0 and 'config_symlink_rejected' in result.stderr:\n    real = Path(os.path.realpath(config_path))\n    # re-invoke with the resolved path, or cp -L to materialize\n    ...","preventionTips":["Never symlink MCP configs across machines — they hold secrets","Pre-check is_symlink before running the merge","Teach dotfiles tools to copy this file instead of linking"],"tags":["filesystem","symlink","security","mcp-config"],"backgroundTag":null,"analyzedSha":"e72fd7aacbbb2236d9b3be2aad7e7178fe94b4bc","analyzedAt":"2026-08-16T08:27:25.381Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}