affaan-m/ECC · error · ValueError

output root must be a directory

Error message

output root must be a directory

What it means

_SafeOutput requires the output root, if it already exists, to be a regular directory (verified via lstat). Any other node type — regular file, FIFO, socket, device — is rejected because the writer opens the root as a directory descriptor and creates children beneath it. The lstat check also ensures symlinked roots were rejected in the prior branch.

Solutions

  1. Delete or rename the conflicting file at the output path, then rerun
  2. Choose a different, directory-shaped output root
  3. Correct the caller/config so the value passed is a directory path, not a file path
  4. mkdir the directory yourself beforehand if you want to guarantee the type

Example fix

# before
out = _SafeOutput(Path('results'))  # results is a regular file

# after
if Path('results').is_file():
    Path('results').unlink()
out = _SafeOutput(Path('results'))
Defensive patterns

Strategy: validation

Validate before calling

def ensure_output_root_free(p):
    if p.exists() and not p.is_dir():
        raise ValueError(f'{p} exists and is not a directory; remove or rename it')

Type guard

def root_is_dir_or_absent(p) -> bool:
    return (not p.exists() and not p.is_symlink()) or (not p.is_symlink() and p.is_dir())

Try / catch

try:
    out = _SafeOutput(root)
except ValueError as e:
    if str(e) == 'output root must be a directory':
        print(f'{root} is a file or special node — remove it or choose another path')
    else:
        raise

Prevention

When it happens

Trigger: Constructing _SafeOutput(Path('results')) where 'results' is an existing regular file (e.g. left over from a previous run that wrote a file at that name) or any non-directory filesystem object.

Common situations: A prior tool run crashed and left a file named like the output dir; a user passed a file path (e.g. results.tar.gz) instead of a directory; build artifacts colliding with the output path.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/ff96d0b470351733. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/workflow.py:180

        for sample in samples
    ):
        raise ValueError("reference style evidence times must be finite and within duration")
    return float(duration)


class _SafeOutput:
    """Descriptor-bound output tree with no-follow traversal and atomic writes."""

    def __init__(self, root: Path) -> None:
        self._root_fd = -1
        if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"):
            raise RuntimeError("secure output requires O_NOFOLLOW and O_DIRECTORY")
        if root.exists() or root.is_symlink():
            metadata = root.lstat()
            if stat.S_ISLNK(metadata.st_mode):
                raise ValueError("output root must not be a symlink")
            if not stat.S_ISDIR(metadata.st_mode):
                raise ValueError("output root must be a directory")
        else:
            if not root.parent.is_dir():
                raise ValueError("output parent directory must already exist")
            root.mkdir(mode=0o700)
        self.root = root
        self._root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
        self._written: list[str] = []

    def close(self) -> None:
        if self._root_fd >= 0:
            os.close(self._root_fd)
            self._root_fd = -1

    def __del__(self) -> None:
        self.close()

    def _open_dir(self, parts: tuple[str, ...], *, create: bool) -> int:
        current = os.dup(self._root_fd)

View on GitHub (pinned to 8321021c54)