666ghj/MiroFish · error · StarHistoryError
output file cannot be a symbolic link
Error message
output file cannot be a symbolic link
What it means
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.
Source
Thrown at scripts/star_history.py:476
raise StarHistoryError("workspace does not exist") from exc
if not root.is_dir():
raise StarHistoryError("workspace is not a directory")
return root
def _safe_target(workspace: Path, relative: Path, create_parent: bool) -> Path:
root = _safe_workspace(workspace)
if relative.is_absolute() or ".." in relative.parts:
raise StarHistoryError("output path escaped the workspace")
current = root
for part in relative.parts[:-1]:
current = current / part
if current.is_symlink():
raise StarHistoryError("output directory cannot be a symbolic link")
target = root / relative
if target.is_symlink():
raise StarHistoryError("output file cannot be a symbolic link")
if create_parent:
try:
target.parent.mkdir(parents=True, exist_ok=True)
except OSError as exc:
raise StarHistoryError("could not create output directory") from exc
current = root
for part in relative.parts[:-1]:
current = current / part
if current.is_symlink():
raise StarHistoryError("output directory cannot be a symbolic link")
if target.is_symlink():
raise StarHistoryError("output file cannot be a symbolic link")
try:
resolved_parent = target.parent.resolve(strict=False)
resolved_parent.relative_to(root)
except (OSError, ValueError) as exc:
raise StarHistoryError("output path escaped the workspace") from exc
return resolved_parent / target.nameView on GitHub (pinned to b5b53acc57)
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
Example fix
# before ln -s /var/cache/history.json .github/star-history/history.json # after rm .github/star-history/history.json # tool recreates a regular file on next write
Defensive patterns
Strategy: validation
Validate before calling
def assert_target_is_regular(root: Path, relative: Path) -> bool:
t = root / relative
return t.exists() and not t.is_symlink() Try / catch
try:
_safe_target(ws, rel, False)
except StarHistoryError as e:
if str(e) == "output file cannot be a symbolic link":
t = ws / rel; print(t.readlink()) # then remove it
raise Prevention
- 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
When it happens
Trigger: _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.
Common situations: 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.
Related errors
- output directory cannot be a symbolic link
- output path escaped the workspace
- could not create output directory
- could not read {label}
- Star count file is missing or unsafe
AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14).
Data as JSON: /api/errors/f464d736d2e5a8e1.
Report an issue: GitHub.