warpdotdev/warp · error · MergeError
config_symlink_rejected
config_symlink_rejected
Error message
config_symlink_rejected
What it means
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.
Source
Thrown at resources/bundled/skills/tui-migrate-setup/scripts/merge_mcp_config.py:114
if path:
current[path[-1]] = value
else:
document.clear()
document.update(value)
def _read_regular_file(path: Path, *, missing_ok: bool) -> tuple[bytes, int | None]:
try:
metadata = path.lstat()
except FileNotFoundError:
if missing_ok:
return b"", None
raise MergeError("source_config_unavailable")
except OSError as error:
raise MergeError("config_unavailable") from error
if stat.S_ISLNK(metadata.st_mode):
raise MergeError("config_symlink_rejected")
if not stat.S_ISREG(metadata.st_mode):
raise MergeError("config_not_regular_file")
if metadata.st_size > MAX_CONFIG_BYTES:
raise MergeError("config_too_large")
flags = os.O_RDONLY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
descriptor = os.open(path, flags)
with os.fdopen(descriptor, "rb") as file:
raw = file.read(MAX_CONFIG_BYTES + 1)
except OSError as error:
raise MergeError("config_unavailable") from error
if len(raw) > MAX_CONFIG_BYTES:
raise MergeError("config_too_large")
return raw, stat.S_IMODE(metadata.st_mode)
View on GitHub (pinned to e72fd7aacb)
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
Example fix
# before ~/.claude.json -> ~/dotfiles/claude.json (symlink) python merge_mcp_config.py --source ~/.claude.json ... # error: config_symlink_rejected # after REAL=$(readlink -f ~/.claude.json) python merge_mcp_config.py --source "$REAL" ...
Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
for p in (Path(args.source), Path(args.destination)):
if p.is_symlink():
raise SystemExit(f'{p} is a symlink; pass the real file or materialize it first') Try / catch
if result.returncode != 0 and 'config_symlink_rejected' in result.stderr:
real = Path(os.path.realpath(config_path))
# re-invoke with the resolved path, or cp -L to materialize
... Prevention
- 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
When it happens
Trigger: 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.
Common situations: Dotfiles-managed machines; multi-account setups sharing one config via symlink; fresh dotfiles clones where the manager symlinks instead of copying.
Related errors
- settings_symlink_rejected
- source_config_unavailable
- config_unavailable
- config_not_regular_file
- config_too_large
AI-assisted analysis of warpdotdev/warp@e72fd7aacb (2026-08-16).
Data as JSON: /api/errors/4ff3919399b134db.
Report an issue: GitHub.