affaan-m/ECC · error · ValueError
artifact path must stay beneath output root
Error message
artifact path must stay beneath output root
What it means
write_json validates that the requested relative artifact path is relative, names a real file component, and contains no '.' or '..' segments. This is a fail-closed path-traversal guard so artifacts can never escape the output root.
Solutions
- Ensure the path passed to write_json is relative to the output root and contains no '..' or '.' segments
- Use pathlib Path arithmetic on a relative base and assert not path.is_absolute() before calling
- Strip/normalize user-supplied filename fragments before constructing artifact paths
Example fix
# before
writer.write_json(os.path.join(base_dir, "manifest.json"), payload) # absolute path
# after
rel = Path("manifest.json")
assert not rel.is_absolute() and ".." not in rel.parts
writer.write_json(str(rel), payload) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def safe_relative(rel: str) -> Path:
p = Path(rel)
if p.is_absolute() or not p.name or any(part in {".", ".."} for part in p.parts):
raise ValueError(f"unsafe artifact path: {rel}")
return p Type guard
def is_safe_artifact_path(rel: str) -> bool:
p = Path(rel)
return (not p.is_absolute() and bool(p.name)
and not any(part in {".", ".."} for part in p.parts)) Try / catch
try:
writer.write_json(relative, payload)
except ValueError as e:
logging.error("rejected artifact path %r: %s", relative, e)
raise Prevention
- Always build artifact paths relative to the output root
- Never pass user input directly into artifact path strings
- Normalize with Path and check parts before calling
When it happens
Trigger: Passing an absolute path (e.g. '/etc/x.json'), a path with a '..' segment, a trailing-segment-only path like 'dir/' (empty name), or a path with '.' components to write_json(relative, payload).
Common situations: Building artifact paths by string concatenation from user/config input; joining an absolute base path with a relative name using os.path.join (which yields the absolute path when the second arg is absolute); template configs that include '../' segments.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- all overlays must be readable local files
- all takes must be readable local files
- artifact path escapes output directory
- base video must be a readable local file
- FileNotFoundError(source)
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/503b39bc1ad73919.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/tasteforge/workflow.py:239
return current
except Exception:
os.close(current)
raise
def prepare(self, directories: tuple[str, ...]) -> None:
"""Validate every known intermediate before the first artifact write."""
opened: list[int] = []
try:
for directory in directories:
opened.append(self._open_dir((directory,), create=True))
finally:
for descriptor in opened:
os.close(descriptor)
def write_json(self, relative: str, payload: Any) -> None:
path = Path(relative)
if path.is_absolute() or not path.name or any(part in {".", ".."} for part in path.parts):
raise ValueError("artifact path must stay beneath output root")
parent_fd = self._open_dir(tuple(path.parts[:-1]), create=False)
temporary = f".{path.name}.tmp-{secrets.token_hex(8)}"
descriptor = -1
try:
try:
existing = os.stat(path.name, dir_fd=parent_fd, follow_symlinks=False)
except FileNotFoundError:
existing = None
if existing is not None and not stat.S_ISREG(existing.st_mode):
raise ValueError(f"output artifact must be a regular file: {relative}")
data = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8") + b"\n"
descriptor = os.open(
temporary,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600,
dir_fd=parent_fd,
)
view = memoryview(data)View on GitHub (pinned to 8321021c54)