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
- Remove the symlink and let the plugin system recreate the real file/directory.
- Exclude the plugin directory from symlink-creating tools (stow, chezmoi, Dropbox link substitution).
- 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
- Never symlink anything inside the built-in plugin snapshot tree.
- Configure dotfile managers and sync tools to leave the plugin directory alone.
- Treat link rejection as a security feature — don't work around it by relaxing the check.
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
- CodewhalePet/1
- could not securely open
- external credential path must name a non-reparse regular…
- file is not a regular single-link file
- is a symbolic link, not a workspace-owned file
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)