affaan-m/ECC · error · ValueError

output parent directory must already exist

Error message

output parent directory must already exist

What it means

When the output root does not exist, _SafeOutput will create it (mode 0o700) but deliberately refuses to create parent directories. If the immediate parent of the requested root is not an existing directory, __init__ raises this error so that path assumptions and permissions stay explicit and no accidental deep trees are created.

Solutions

  1. Create the parent directory first: Path('a/b').mkdir(parents=True, exist_ok=True) before constructing _SafeOutput
  2. Verify/fix the output path spelling in your config or CLI arguments
  3. Have the calling script (e.g. run_workflow's CLI entry) prepare the base output directory before invoking _SafeOutput
  4. Run from the intended working directory so relative paths resolve to existing parents

Example fix

# before
out = _SafeOutput(Path('artifacts/run1'))  # artifacts/ does not exist

# after
Path('artifacts').mkdir(parents=True, exist_ok=True)
out = _SafeOutput(Path('artifacts/run1'))
Defensive patterns

Strategy: validation

Validate before calling

def ensure_parent_exists(root):
    if not root.parent.is_dir():
        root.parent.mkdir(parents=True, exist_ok=True)

Type guard

def parent_ready(p) -> bool:
    return p.parent.is_dir()

Try / catch

try:
    out = _SafeOutput(root)
except ValueError as e:
    if 'parent directory must already exist' in str(e):
        root.parent.mkdir(parents=True, exist_ok=True)
        out = _SafeOutput(root)
    else:
        raise

Prevention

When it happens

Trigger: _SafeOutput(Path('a/b/c')) where 'a/b' or 'a/b/c''s parent does not exist; passing outputs/<run-id>/... when outputs/ was deleted; a typo in the parent directory name.

Common situations: CI workspaces where prior artifact dirs were cleaned before the run; user-supplied output paths with non-existent nested parents; typos like 'ouputs/run1'.

Related errors


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

Appendix: source

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

    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)
        try:
            for part in parts:
                if not part or part in {".", ".."} or "/" in part:

View on GitHub (pinned to 8321021c54)