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

  1. Materialize the file: cp -L <path> <tmp> && mv <tmp> <path>, replacing the symlink with its content
  2. Point the argument directly at the real file the symlink resolves to (check with readlink -f)
  3. 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

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


AI-assisted analysis of warpdotdev/warp@e72fd7aacb (2026-08-16). Data as JSON: /api/errors/4ff3919399b134db. Report an issue: GitHub.