abhigyanpatwari/GitNexus · error · SandboxError

Compound Engineering plugin manifest directory must be real:

Error message

Compound Engineering plugin manifest directory must be real: {manifest_root}

What it means

Even when .claude-plugin/ can be lstat'd, the harness rejects it if the mode is a symlink (stat.S_ISLNK) or not a directory (not stat.S_ISDIR). Real directories are required so a symlink cannot redirect manifest reads outside the bounded source.

Source

Thrown at eval/workflow_bench/runtime_mounts.py:298

        if lowered.startswith(".env.") or lowered.startswith(".npmrc."):
            return True
        if lowered.endswith(_SECRET_SUFFIXES) or any(marker in lowered for marker in _SECRET_NAME_MARKERS):
            return True
    return False


def _plugin_files(source: Path) -> Iterator[tuple[PurePosixPath, Path]]:
    """Yield only allowlisted plugin files in stable order."""

    manifest_root = source / ".claude-plugin"
    try:
        manifest_root_metadata = manifest_root.lstat()
    except OSError as exc:
        raise SandboxError(
            f"Compound Engineering plugin manifest directory is unavailable: {manifest_root}: {exc}"
        ) from exc
    if stat.S_ISLNK(manifest_root_metadata.st_mode) or not stat.S_ISDIR(manifest_root_metadata.st_mode):
        raise SandboxError(f"Compound Engineering plugin manifest directory must be real: {manifest_root}")
    required_manifest = _ALLOWED_PLUGIN_MANIFESTS[0]
    manifest_path = source / Path(*required_manifest.parts)
    if not manifest_path.exists():
        raise SandboxError(f"Compound Engineering plugin manifest is missing: {manifest_path}")
    for relative in _ALLOWED_PLUGIN_MANIFESTS:
        candidate = source / Path(*relative.parts)
        if candidate.exists():
            yield relative, candidate

    skills = source / "skills"
    if not skills.exists():
        raise SandboxError(f"Compound Engineering plugin skills directory is missing: {skills}")

    def walk(directory: Path, relative_dir: PurePosixPath) -> Iterator[tuple[PurePosixPath, Path]]:
        try:
            with os.scandir(directory) as scanned:
                entries = sorted(scanned, key=lambda entry: entry.name)
        except OSError as exc:

View on GitHub (pinned to d540b00184)

Solutions

  1. Replace the symlink with a real directory: `rm <plugin>/.claude-plugin && mkdir <plugin>/.claude-plugin && cp -r <real>/.claude-plugin/. <plugin>/.claude-plugin/`.
  2. Re-extract the plugin tarball with `tar --no-same-owner -xhf` (the `-h` dereferences symlinks at extraction time).
Defensive patterns

Strategy: validation

Validate before calling

import stat
from pathlib import Path

def manifest_dir_is_real(source: Path) -> bool:
    p = source / ".claude-plugin"
    try:
        mode = p.lstat().st_mode
    except OSError:
        return False
    return not stat.S_ISLNK(mode) and stat.S_ISDIR(mode)

Try / catch

try:
    list(_plugin_files(source))
except SandboxError as exc:
    if "manifest directory must be real" in str(exc):
        # replace the symlink with a real directory
        ...
    raise

Prevention

When it happens

Trigger: source/.claude-plugin is a symlink to another location, a regular file, or any non-directory file type.

Common situations: Plugin shipped with a convenience symlink; operator symlinked .claude-plugin from a shared location for testing; tarball extraction preserved a symlink instead of materializing it.

Related errors


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