{"record":{"id":"f464d736d2e5a8e1","repo":"666ghj/MiroFish","slug":"output-file-cannot-be-a-symbolic-link","errorCode":null,"errorMessage":"output file cannot be a symbolic link","messagePattern":"output file cannot be a symbolic link","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":476,"sourceCode":"        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)\n    except (OSError, ValueError) as exc:\n        raise StarHistoryError(\"output path escaped the workspace\") from exc\n    return resolved_parent / target.name","sourceCodeStart":458,"sourceCodeEnd":494,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L458-L494","documentation":"Raised by _safe_target when the final output file itself (e.g. history.json) is a symbolic link. Even if all parent directories are clean, the tool refuses to write through a symlinked file because the link could point outside the workspace, defeating the containment guarantee.","triggerScenarios":"_safe_target (directly, or via save_state/load_state) is called and root/relative — typically .github/star-history/history.json — has lstat indicating a symlink. Checked both before parent creation (line 475) and again after (line 487) to catch TOCTOU races.","commonSituations":"Leftover 'ln -s' convenience links to a previous state location, dotfile managers that link individual JSON files, or an attacker planting a last-component symlink so an atomic rename clobbers a file outside the workspace.","solutions":["Check: readlink .github/star-history/history.json","Move the link aside and let the tool recreate a regular file (mv history.json history.json.bak; the tool writes a fresh file)","Keep the real file in the workspace; if you need a copy elsewhere, copy it after the tool runs","Audit who created the link if you did not"],"exampleFix":"# before\nln -s /var/cache/history.json .github/star-history/history.json\n# after\nrm .github/star-history/history.json\n# tool recreates a regular file on next write","handlingStrategy":"validation","validationCode":"def assert_target_is_regular(root: Path, relative: Path) -> bool:\n    t = root / relative\n    return t.exists() and not t.is_symlink()","typeGuard":null,"tryCatchPattern":"try:\n    _safe_target(ws, rel, False)\nexcept StarHistoryError as e:\n    if str(e) == \"output file cannot be a symbolic link\":\n        t = ws / rel; print(t.readlink())  # then remove it\n    raise","preventionTips":["Keep history.json a plain file in-repo","Exclude state files from dotfile managers","Audit unexpected links: find .github -type l","Copy state elsewhere after writes instead of linking beforehand"],"tags":["filesystem","symlink","security","state"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}