Panniantong/Agent-Reach · error · PrivatePathError

{label}不能经过符号链接:{current}

Error message

{label}不能经过符号链接:{current}

What it means

Raised by ensure_no_symlink_path when any existing component of the path being checked is a symbolic link. Agent Reach hardened its private-directory and file-read paths against symlink traversal attacks (TOCTOU / link-swapping), so private data directories and read targets must not traverse symlinks. The message is in Chinese: '{label} cannot pass through a symlink: {current}'.

Source

Thrown at agent_reach/utils/paths.py:46

    expanded = os.path.expanduser("~")
    if expanded and expanded != "~":
        return Path(expanded)
    return Path.home()


def ensure_no_symlink_path(path: str | Path, label: str = "路径") -> Path:
    """Reject any existing symlink component without resolving the path."""
    target = Path(path)
    absolute = Path(os.path.abspath(os.fspath(target)))
    current = Path(absolute.anchor)
    for part in absolute.parts[1:]:
        current /= part
        try:
            mode = os.lstat(current).st_mode
        except FileNotFoundError:
            continue
        if stat.S_ISLNK(mode):
            raise PrivatePathError(f"{label}不能经过符号链接:{current}")
    return target


def make_private_dir(path: str | Path) -> Path:
    """Create a directory restricted to the current user where supported."""
    target = ensure_no_symlink_path(path, "私密目录")
    target.mkdir(mode=0o700, parents=True, exist_ok=True)
    ensure_no_symlink_path(target, "私密目录")
    if sys.platform != "win32":
        flags = (
            os.O_RDONLY
            | getattr(os, "O_DIRECTORY", 0)
            | getattr(os, "O_NOFOLLOW", 0)
        )
        dir_fd = os.open(target, flags)
        try:
            ensure_no_symlink_path(target, "私密目录")
            if hasattr(os, "fchmod"):

View on GitHub (pinned to 93ae1d18c3)

Solutions

  1. Resolve symlinks before passing the path: pass Path(path).resolve() so the checked path contains real components
  2. Move the real directory into place instead of symlinking it (e.g. point the dotfiles tool the other way)
  3. Set XDG_CONFIG_HOME to a real (non-symlinked) directory
  4. On macOS, prefer real paths like /private/tmp over /tmp aliases when constructing paths

Example fix

# before
from agent_reach.utils.paths import make_private_dir
d = make_private_dir("~/.config/agent-reach")  # ~/.config is a symlink

# after
from pathlib import Path
d = make_private_dir(Path("~/.config/agent-reach").expanduser().resolve())
Defensive patterns

Strategy: validation

Validate before calling

import os, stat
from pathlib import Path

def path_has_symlink(path: str | Path) -> bool:
    absolute = Path(os.path.abspath(os.fspath(path)))
    current = Path(absolute.anchor)
    for part in absolute.parts[1:]:
        current /= part
        try:
            if stat.S_ISLNK(os.lstat(current).st_mode):
                return True
        except FileNotFoundError:
            return False
    return False

# usage: assert not path_has_symlink(my_dir)

Try / catch

from agent_reach.utils.paths import PrivatePathError, make_private_dir
try:
    d = make_private_dir(target)
except PrivatePathError:
    d = make_private_dir(Path(target).resolve())  # retry on the resolved path

Prevention

When it happens

Trigger: Calling make_private_dir() or the private read helper with a path where an intermediate component (e.g. ~/.config being a symlink, common on macOS where ~/Library/... is linked, or a dotfiles-managed symlink) is a link. The final component being a symlink also triggers it since every part is lstat'ed.

Common situations: Users who symlink ~/.config to a dotfiles repo; nix/homebrew setups where config dirs are links; XDG_CONFIG_HOME pointing at a symlinked path; test fixtures that create temp dirs via symlinks; macOS where /tmp is a symlink to /private/tmp (use realpath before calling).

Related errors


AI-assisted analysis of Panniantong/Agent-Reach@93ae1d18c3 (2026-08-14). Data as JSON: /api/errors/0166851c459cf6ed. Report an issue: GitHub.