crewAIInc/crewAI · error · ValueError

Symlink escapes allowed directory: {self.path}

Error message

Symlink escapes allowed directory: {self.path}

What it means

Validation error from FilePath: the path is a symlink, and resolving it yields a location that does not start with the current working directory (Path.cwd().resolve()). The guard is string-prefix based against CWD, not against an explicit allow-list, so even a safe symlink inside your project fails whenever your process CWD is elsewhere (e.g. server started from / while files live in /app). It exists to stop symlink-based escape of the allowed directory.

Source

Thrown at lib/crewai-files/src/crewai_files/core/sources.py:231

        description="Maximum file size in bytes.",
    )
    _content: bytes | None = PrivateAttr(default=None)
    _content_type: str = PrivateAttr()

    @model_validator(mode="after")
    def _validate_file_exists(self) -> FilePath:
        """Validate that the file exists, is secure, and within size limits."""
        from crewai_files.processing.exceptions import FileTooLargeError

        path_str = str(self.path)
        if ".." in path_str:
            raise ValueError(f"Path traversal not allowed: {self.path}")

        if self.path.is_symlink():
            resolved = self.path.resolve()
            cwd = Path.cwd().resolve()
            if not str(resolved).startswith(str(cwd)):
                raise ValueError(f"Symlink escapes allowed directory: {self.path}")

        if not self.path.exists():
            raise ValueError(f"File not found: {self.path}")
        if not self.path.is_file():
            raise ValueError(f"Path is not a file: {self.path}")

        actual_size = self.path.stat().st_size
        if actual_size > self.max_size_bytes:
            raise FileTooLargeError(
                f"File exceeds max size ({actual_size} > {self.max_size_bytes})",
                file_name=str(self.path),
                actual_size=actual_size,
                max_size=self.max_size_bytes,
            )

        self._content_type = detect_content_type_from_path(self.path, self.path.name)
        return self

View on GitHub (pinned to 754d7323be)

Solutions

  1. chdir to the intended root (or launch the process with the correct working directory) so CWD is a prefix of the resolved symlink target.
  2. Replace symlinks with real files or copy the target into the project.
  3. Pass a direct, non-symlink path to the real file's location if it is reachable.

Example fix

# before (service runs with cwd=/):
src = FilePath(path=Path("/app/data/linked.txt"))  # symlink -> /app/assets/x.txt

# after:
import os
os.chdir("/app")  # or fix the unit file WorkingDirectory=/app
src = FilePath(path=Path("data/linked.txt"))
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def symlink_ok(p: Path) -> bool:
    if not p.is_symlink():
        return True
    return str(p.resolve()).startswith(str(Path.cwd().resolve()))

Try / catch

try:
    FilePath(path=p)
except ValidationError as e:
    if "Symlink escapes" in str(e):
        FilePath(path=p.resolve())  # use the real target directly

Prevention

When it happens

Trigger: FilePath(path=Path("data/linked.txt")) where linked.txt -> /etc/secret, or a legitimate in-project symlink while the process was started from a directory that is not a prefix of the resolved target (systemd services, Docker WORKDIR mismatches).

Common situations: Monorepo symlinks to shared assets; Docker containers where CWD differs from the app dir; daemon processes launched from /; symlinked home-dir configs.

Related errors


AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15). Data as JSON: /api/errors/3687a8ef9af019b1. Report an issue: GitHub.