abhigyanpatwari/GitNexus · error · SandboxError

Compound Engineering plugin file must be regular and non-sym

Error message

Compound Engineering plugin file must be regular and non-symlink: {path}

What it means

Raised by _bounded_plugin_bytes when a file inside the Compound Engineering comparator plugin source tree is a symlink or anything other than a regular file (fifo, socket, device). The harness refuses to follow last-component symlinks so a malicious or accidental link cannot point content outside the validated plugin root. The check uses lstat, so the link itself is the violation, not its target.

Source

Thrown at eval/workflow_bench/runtime_mounts.py:356

            continue
        try:
            metadata = directory.lstat()
        except OSError as exc:
            raise SandboxError(f"Compound Engineering plugin component is unreadable: {directory}: {exc}") from exc
        if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
            raise SandboxError(f"Compound Engineering plugin component must be a real directory: {directory}")
        yield from walk(directory, PurePosixPath(name))


def _bounded_plugin_bytes(path: Path) -> tuple[bytes, bool]:
    """Read one stable regular file without following a last-component symlink."""

    try:
        before = path.lstat()
    except OSError as exc:
        raise SandboxError(f"Compound Engineering plugin file is unreadable: {path}: {exc}") from exc
    if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
        raise SandboxError(f"Compound Engineering plugin file must be regular and non-symlink: {path}")
    if before.st_size > MAX_CE_PLUGIN_FILE_BYTES:
        raise SandboxError(f"Compound Engineering plugin file exceeds the per-file limit: {path}")
    descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
    try:
        opened = os.fstat(descriptor)
        if (opened.st_dev, opened.st_ino) != (before.st_dev, before.st_ino) or not stat.S_ISREG(opened.st_mode):
            raise SandboxError(f"Compound Engineering plugin file changed during validation: {path}")
        chunks: list[bytes] = []
        remaining = MAX_CE_PLUGIN_FILE_BYTES + 1
        while remaining > 0:
            chunk = os.read(descriptor, min(64 * 1024, remaining))
            if not chunk:
                break
            chunks.append(chunk)
            remaining -= len(chunk)
        payload = b"".join(chunks)
        after = os.fstat(descriptor)
    finally:

View on GitHub (pinned to d540b00184)

Solutions

  1. Replace every symlink in the plugin tree with a real file copy: 'cp -RL <plugin_src> <fixed_plugin_src>' then point --ce-plugin-dir at the copy.
  2. Find offenders before running: 'find <plugin_dir> -type l' and resolve each by copying the target file in place.
  3. Repackage the plugin so shared files are physically duplicated per skill rather than symlinked.
  4. If a directory component is involved, confirm it is a real directory (error 540's sibling at line 344 fires for symlinked dirs).

Example fix

# before
ln -s ../../shared/prompts.md skills/ce-plan/prompts.md
# after
cp ../../shared/prompts.md skills/ce-plan/prompts.md
Defensive patterns

Strategy: validation

Validate before calling

import os, stat
from pathlib import Path

def assert_plugin_files_regular(root: Path) -> list[Path]:
    bad: list[Path] = []
    allowed = ("skills", "scripts", "assets", ".claude-plugin")
    for base in allowed:
        d = root / base
        if not d.exists():
            continue
        for dirpath, _dirs, files in os.walk(d, followlinks=False):
            for name in files:
                p = Path(dirpath, name)
                mode = p.lstat().st_mode
                if stat.S_ISLNK(mode) or not stat.S_ISREG(mode):
                    bad.append(p)
    if bad:
        raise SystemExit(f"non-regular plugin files: {bad}")
    return bad

# run before --ce-plugin-dir
assert_plugin_files_regular(Path("ce-plugin"))

Type guard

import stat
from pathlib import Path

def is_regular_nonsymlink(path: Path) -> bool:
    try:
        mode = path.lstat().st_mode
    except OSError:
        return False
    return stat.S_ISREG(mode) and not stat.S_ISLNK(mode)

Try / catch

# Not recoverable mid-run. Pre-validate the tree (above) and fix the source.
# Catching SandboxError only lets you report and exit cleanly:
try:
    staged_ce_plugin_snapshot(config, destination_parent=runtime_root)
except SandboxError as exc:
    log.error("plugin staging failed: %s", exc)
    raise

Prevention

When it happens

Trigger: A file under one of _ALLOWED_PLUGIN_DIRS (skills, scripts, assets) or .claude-plugin is a symbolic link, or is a non-regular inode. Common producers: 'npm link'/'pnpm link' dev installs, 'stow', a wrapper that symlinks shared assets, or a checkout where node_modules-style dedup created symlinks.

Common situations: Authoring the CE plugin in a monorepo and symlinking shared code into skills/; using 'ln -s' to assemble a release tree; a CI step that creates relative symlinks for path compatibility; shipping the plugin from a read-only mirror via symlinks.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/f669e8e7668ee8bf. Report an issue: GitHub.