affaan-m/ECC · error · ValueError
output path contains an invalid component
Error message
output path contains an invalid component
What it means
_SafeOutput._open_dir walks the output tree component by component using dir_fd-relative operations, and validates each component name. A component that is empty, is '.' or '..', or contains '/' (path-separator injection) is rejected to guarantee the final path cannot escape the anchored root directory. This is a path-traversal guard for every prepared/written artifact.
Solutions
- Sanitize components: reject/split any name containing '/', and drop '', '.', '..' segments before calling the API
- Use pathlib to derive pure relative names: name = Path(untrusted).name, then pass parts individually
- Never pass absolute paths into write_json/prepare — the root is the anchor; pass paths relative to it
- Validate user-supplied artifact names against a strict pattern (e.g. ^[A-Za-z0-9._-]+$)
Example fix
# before
safe.write_json(user_supplied_relpath, data) # e.g. '../etc/evil.json'
# after
parts = tuple(p for p in PurePosixPath(user_supplied_relpath).parts
if p not in ('', '.', '..') and '/' not in p)
if not parts or any(p.startswith('/') for p in parts):
raise ValueError('unsafe artifact path')
safe.write_json(parts, data) Defensive patterns
Strategy: validation
Validate before calling
import re
SAFE_COMPONENT = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]*$')
def safe_parts(relpath):
parts = [p for p in PurePosixPath(relpath).parts]
if not parts or any(not SAFE_COMPONENT.match(p) for p in parts):
raise ValueError(f'unsafe artifact path: {relpath!r}')
return tuple(parts) Type guard
def is_safe_component(p: str) -> bool:
return bool(p) and p not in ('.', '..') and '/' not in p and not p.startswith('/') Try / catch
try:
safe.write_json(parts, data)
except ValueError as e:
if 'invalid component' in str(e):
print(f'Rejecting untrusted path: {parts!r} ({e})')
else:
raise Prevention
- Never pass absolute or user-controlled raw paths into write_json/prepare
- Normalize with PurePosixPath(...).name to strip traversal segments
- Allowlist artifact names with a strict regex
- Treat filenames from probe/metadata as untrusted input
When it happens
Trigger: Calling prepare/write_json/artifact_metadata with a relative path containing '../', a leading '/', an embedded '/', or an empty segment (e.g. 'out//file.json', './x', 'sub/../file.json').
Common situations: Building paths from user input or untrusted metadata (filenames from probe data); joining an absolute path with the output root and passing the concatenation; empty filename variables from failed templates.
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
- artifact path escapes output directory
- Refusing unsafe repair source metadata: sources must stay…
- spec.file must keep generated files directly inside the…
- artifact path must stay beneath output root
- artifact permits provider execution
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/679781021384571a.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/tasteforge/workflow.py:202
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:
raise ValueError("output path contains an invalid component")
try:
metadata = os.stat(part, dir_fd=current, follow_symlinks=False)
except FileNotFoundError:
if not create:
raise ValueError(f"missing output directory: {part}") from None
os.mkdir(part, mode=0o700, dir_fd=current)
metadata = os.stat(part, dir_fd=current, follow_symlinks=False)
if stat.S_ISLNK(metadata.st_mode):
raise ValueError(f"output directory must not be a symlink: {part}")
if not stat.S_ISDIR(metadata.st_mode):
raise ValueError(f"output intermediate must be a directory: {part}")
child = os.open(
part,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=current,
)
os.close(current)
current = childView on GitHub (pinned to 8321021c54)