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
- Delete or rename the conflicting file at the output path, then rerun
- Choose a different, directory-shaped output root
- Correct the caller/config so the value passed is a directory path, not a file path
- 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
- Verify the output path is not occupied by a leftover file before running
- Never pass a file path where a directory path is expected
- Clean failed runs fully so output names do not collide
- Check for special files (FIFOs/sockets) in artifact directories
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
- Nasiko cannot install directly into a filesystem root.
- output bundle root must be a directory
- all overlays must be readable local files
- all takes must be readable local files
- artifact path must stay beneath output root
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)