666ghj/MiroFish · error · StarHistoryError

output directory cannot be a symbolic link

Error message

output directory cannot be a symbolic link

What it means

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.

Source

Thrown at scripts/star_history.py:473

    try:
        root = workspace.resolve(strict=True)
    except OSError as exc:
        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)

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Run: ls -la .github .github/star-history in the workspace to find the offending link
  2. Replace the symlink with a real directory (mkdir + copy contents, then rm the link)
  3. Re-run the command; if it recurs, check for processes recreating the link
  4. If you intentionally need linked storage, bind-mount instead of symlinking

Example fix

# before
.github -> ../shared-config/.github
# after (real directory)
mkdir -p .github/star-history && cp -r ../shared-config/.github/* .github/
Defensive patterns

Strategy: validation

Validate before calling

def assert_no_dir_symlinks(root: Path, relative: Path) -> None:
    cur = root.resolve(strict=True)
    for part in relative.parts[:-1]:
        cur = cur / part
        if cur.is_symlink():
            raise RuntimeError(f"symlinked directory: {cur}")

Try / catch

try:
    target = _safe_target(ws, rel, create_parent=True)
except StarHistoryError as e:
    if str(e) == "output directory cannot be a symbolic link":
        fix_and_report(e)  # inspect .github chain
    raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/8e4d0886ce898455. Report an issue: GitHub.