affaan-m/ECC · error · ValueError
Symlinks are not accepted
Error message
Symlinks are not accepted: {part} What it means
_path walks the path and all of its parents and refuses any component that is a symlink, before resolving '..'. This prevents symlink-based path traversal and ensures fingerprints/provenance bind to the real file location. Any symlinked component raises ValueError naming the offending path component.
Solutions
- Use path.resolve() (or os.path.realpath) to get the real path and pass that instead.
- Move or copy assets into a non-symlinked location and reference them directly.
- On macOS, use /private/tmp instead of /tmp when staging assets.
- Check which component is flagged in the message and replace that symlink with a real directory.
Example fix
// before
ingest_assets(sp, paths=["/tmp/renders/clip.mp4"]) # /tmp is a symlink
// after
from pathlib import Path
real = Path("/tmp/renders/clip.mp4").resolve() # e.g. /private/tmp/renders/clip.mp4
ingest_assets(sp, paths=[str(real)]) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def assert_no_symlinks(p: str):
path = Path(p).expanduser()
for part in (path, *path.parents):
if part.is_symlink():
raise ValueError(f"symlink component: {part}") Type guard
def is_symlink_free(p: str) -> bool:
from pathlib import Path
path = Path(p).expanduser()
return not any(part.is_symlink() for part in (path, *path.parents)) Try / catch
try:
assets = ingest_assets(sp, paths=paths)
except ValueError as e:
if "Symlinks are not accepted" in str(e):
paths = [str(Path(p).resolve()) for p in paths]
assets = ingest_assets(sp, paths=paths)
else:
raise Prevention
- Always pass Path(...).resolve() outputs to the asset API.
- On macOS, stage assets under /private/tmp, not /tmp.
- Avoid symlink-farm project layouts (e.g. stow) for asset directories.
When it happens
Trigger: Passing a path through a symlinked directory (e.g. /tmp being a symlink to /private/tmp on macOS, or ~/assets pointing elsewhere) to ingest_assets/validate_assets/_bundle/_provenance.
Common situations: macOS /tmp → /private/tmp; home directories symlinked into cloud-sync folders; dotfile-managed directories (stow) that are symlinks; project dirs reached via a convenience symlink.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- gate.variant_invalid
- Invalid ECC repo root: missing package.json at
- Nasiko executable must be a regular file, not a symlink.
- Nasiko install directory must be a real directory, not a…
- output artifact must be a regular file
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/257199d5fb880bfb.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/tasteforge/assets.py:39
def _text(value: Any, name: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f'{name} must be a nonempty string')
return value
def _path(value: Any, base: Path) -> Path:
raw = _text(str(value) if isinstance(value, Path) else value, 'path')
if '://' in raw or raw.startswith(('file:', 'http:', 'https:')):
raise ValueError('Only local filesystem paths are supported')
path = Path(raw).expanduser()
if not path.is_absolute():
path = base / path
# Check before resolving '..' to avoid hiding a symlink in the path.
for part in (path, *path.parents):
if part.is_symlink():
raise ValueError(f'Symlinks are not accepted: {part}')
return path.resolve()
def _fingerprint(path: Path, modality: str | None = None) -> dict[str, Any]:
_path(path, Path.cwd())
try:
before = path.stat()
if not stat.S_ISREG(before.st_mode):
raise ValueError(f'Not a regular file: {path}')
flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | os.O_NONBLOCK
fd = os.open(path, flags)
with os.fdopen(fd, 'rb') as stream:
opened = os.fstat(stream.fileno())
if not stat.S_ISREG(opened.st_mode):
raise ValueError(f'Not a regular file: {path}')
digest = hashlib.sha256()
header = stream.read(12)
digest.update(header)View on GitHub (pinned to 8321021c54)