Hmbown/CodeWhale · error

built-in plugin path may not be a symbolic link or reparse…

Error message

built-in plugin path may not be a symbolic link or reparse point: {path}

What it means

reject_symlink refuses to write through any path component target that is a symbolic link or reparse point, using symlink_metadata plus metadata_is_link_or_reparse. This mirrors discovery's rule when scanning plugin roots and prevents an attacker from redirecting built-in plugin writes (e.g. via a planted symlink) to arbitrary locations.

Solutions

  1. Remove the symlink and let the plugin system recreate the real file/directory.
  2. Exclude the plugin directory from symlink-creating tools (stow, chezmoi, Dropbox link substitution).
  3. If you intentionally linked plugin storage, move the real storage location instead of linking inside the snapshot tree.

Example fix

// before
ln -s /mnt/bigdisk/plugins ~/.local/share/codewhale/plugins/builtin
// after
rm ~/.local/share/codewhale/plugins/builtin
mkdir ~/.local/share/codewhale/plugins/builtin   # real directory
Defensive patterns

Strategy: validation

Validate before calling

let md = std::fs::symlink_metadata(path)?;
if md.file_type().is_symlink() { eprintln!("{path:?} is a link; remove it before materializing plugins"); }

Type guard

fn not_a_link(p: &Path) -> bool {
    std::fs::symlink_metadata(p).map(|m| !m.file_type().is_symlink()).unwrap_or(true) // missing is fine, will be created
}

Try / catch

match materialize_at_home() {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput && e.to_string().contains("symbolic link") => {
        eprintln!("remove the symlink inside the plugin tree and retry");
    }
    other => other?,
}

Prevention

When it happens

Trigger: materialize_at_home or write_bundle encounters a symlink/reparse point at a path it is about to create or write inside the built-in plugin snapshot tree.

Common situations: Someone (or a dotfile manager) replaced a plugin directory or file with a symlink; sync tools substituted links; a malicious repo planted links in a plugin path.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/9069c0b7d8509261. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/plugins/builtin.rs:413

            MOVEFILE_WRITE_THROUGH,
        )
    }
    .map_err(|_| io::Error::last_os_error())
}

#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
fn publish_snapshot(_source: &Path, _destination: &Path) -> io::Result<()> {
    Err(io::Error::new(
        io::ErrorKind::Unsupported,
        "atomic built-in snapshot publication is unsupported on this platform",
    ))
}

/// Refuse to write through a symbolic link or reparse point, the same rule
/// [`super::discovery`] applies when it scans a plugin root.
fn reject_symlink(path: &Path) -> io::Result<()> {
    match fs::symlink_metadata(path) {
        Ok(metadata) if metadata_is_link_or_reparse(&metadata) => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!(
                "built-in plugin path may not be a symbolic link or reparse point: {}",
                path.display()
            ),
        )),
        Ok(_) => Ok(()),
        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(error),
    }
}

#[cfg(test)]
#[path = "builtin_tests.rs"]
mod snapshot_tests;

#[cfg(test)]
mod tests {

View on GitHub (pinned to 73e0f67d83)