{"record":{"id":"8e4d0886ce898455","repo":"666ghj/MiroFish","slug":"output-directory-cannot-be-a-symbolic-link","errorCode":null,"errorMessage":"output directory cannot be a symbolic link","messagePattern":"output directory cannot be a symbolic link","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":473,"sourceCode":"    try:\n        root = workspace.resolve(strict=True)\n    except OSError as exc:\n        raise StarHistoryError(\"workspace does not exist\") from exc\n    if not root.is_dir():\n        raise StarHistoryError(\"workspace is not a directory\")\n    return root\n\n\ndef _safe_target(workspace: Path, relative: Path, create_parent: bool) -> Path:\n    root = _safe_workspace(workspace)\n    if relative.is_absolute() or \"..\" in relative.parts:\n        raise StarHistoryError(\"output path escaped the workspace\")\n\n    current = root\n    for part in relative.parts[:-1]:\n        current = current / part\n        if current.is_symlink():\n            raise StarHistoryError(\"output directory cannot be a symbolic link\")\n    target = root / relative\n    if target.is_symlink():\n        raise StarHistoryError(\"output file cannot be a symbolic link\")\n    if create_parent:\n        try:\n            target.parent.mkdir(parents=True, exist_ok=True)\n        except OSError as exc:\n            raise StarHistoryError(\"could not create output directory\") from exc\n        current = root\n        for part in relative.parts[:-1]:\n            current = current / part\n            if current.is_symlink():\n                raise StarHistoryError(\"output directory cannot be a symbolic link\")\n        if target.is_symlink():\n            raise StarHistoryError(\"output file cannot be a symbolic link\")\n    try:\n        resolved_parent = target.parent.resolve(strict=False)\n        resolved_parent.relative_to(root)","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L455-L491","documentation":"Raised by _safe_target in scripts/star_history.py when a directory component of a relative output path (e.g. '.github/star-history/' inside history.json's path) is a symbolic link. The tool refuses to traverse symlinks anywhere in the output directory chain so a compromised or misconfigured workspace cannot redirect state writes outside the workspace root.","triggerScenarios":"Calling any API that resolves an output target (_safe_target, or indirectly load_state/save_state with a workspace whose .github or .github/star-history is a symlinked directory). Both the pre-check (line 472) and the post-mkdir re-check (line 485) raise this message.","commonSituations":"Developers symlink .github into a shared dotfiles repo, use monorepo workspaces where .github is a link to a templates dir, or CI caches that materialize directories as links. Also a symlink-swap attack between the mkdir and the write.","solutions":["Run: ls -la .github .github/star-history in the workspace to find the offending link","Replace the symlink with a real directory (mkdir + copy contents, then rm the link)","Re-run the command; if it recurs, check for processes recreating the link","If you intentionally need linked storage, bind-mount instead of symlinking"],"exampleFix":"# before\n.github -> ../shared-config/.github\n# after (real directory)\nmkdir -p .github/star-history && cp -r ../shared-config/.github/* .github/","handlingStrategy":"validation","validationCode":"def assert_no_dir_symlinks(root: Path, relative: Path) -> None:\n    cur = root.resolve(strict=True)\n    for part in relative.parts[:-1]:\n        cur = cur / part\n        if cur.is_symlink():\n            raise RuntimeError(f\"symlinked directory: {cur}\")","typeGuard":null,"tryCatchPattern":"try:\n    target = _safe_target(ws, rel, create_parent=True)\nexcept StarHistoryError as e:\n    if str(e) == \"output directory cannot be a symbolic link\":\n        fix_and_report(e)  # inspect .github chain\n    raise","preventionTips":["Never symlink .github or .github/star-history; use real directories","Bind-mount shared config instead of symlinking","Run ls -la .github before invoking state-writing steps","Lock the workspace so cleanup jobs cannot swap dirs mid-run"],"tags":["filesystem","symlink","security","state"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}