Hmbown/CodeWhale · error
refusing to mutate symlinked Codewhale skills path component
Error message
refusing to mutate symlinked Codewhale skills path component {} What it means
checked_real_directory walks skills path components using symlink_metadata (deliberately not following links) and refuses to mutate if any component is a symlink. Following the link first could redirect a CodeWhale-owned root to an attacker-chosen write target, so symlinked components are rejected outright.
Solutions
- Replace the symlink with a real directory (copy contents back in)
- Symlink the individual skills inside the directory instead of any component of the owned chain
- Point configuration at the real location and let CodeWhale create its own directory there
Example fix
// before ln -s ~/dotfiles/skills ~/.codewhale/skills // after rm ~/.codewhale/skills mkdir ~/.codewhale/skills cp -r ~/dotfiles/skills/* ~/.codewhale/skills/
Defensive patterns
Strategy: validation
Validate before calling
use std::fs::symlink_metadata;
for comp in [anchor.join(".codewhale"), anchor.join(".codewhale").join("skills")] {
if let Ok(meta) = symlink_metadata(&comp) {
assert!(!meta.file_type().is_symlink(), "{} is a symlink", comp.display());
}
} Type guard
fn is_real_dir(p: &Path) -> bool {
std::fs::symlink_metadata(p).map(|m| !m.file_type().is_symlink() && m.is_dir()).unwrap_or(false)
} Try / catch
match install_remote(anchor, name) {
Err(e) if e.to_string().contains("symlinked") => eprintln!("replace the symlink in the skills path with a real directory"),
other => other?,
} Prevention
- Do not symlink ~/.codewhale or its children to dotfiles/cloud-sync locations
- Sync skill directories by copying or bind-mounting real directories
- Check setups with `ls -la ~/.codewhale` for symlinks before installing
When it happens
Trigger: Installing, updating, or removing a skill where the anchor, .codewhale dir, skills dir, or any intermediate path component is a symlink (raised via validate_owned_target_chain, create_owned_directory, prepare_owned_target, or validate_owned_child).
Common situations: Users symlinking ~/.codewhale/skills to a dotfiles-managed or Dropbox-synced location; setups where the whole .codewhale directory is a symlink.
Related errors
- audited skill path does not match owned package
- owned skill path escapes direct root
- refusing to copy symlink
- refusing to import unsafe external skill package
- audited owned root does not match mutation target
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/306a718996310497.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/skills/mutation.rs:149
fn owned_anchor<'a>(
workspace: &'a Path,
home: Option<&'a Path>,
target: SkillTargetScope,
) -> Result<&'a Path> {
match target {
SkillTargetScope::Project => Ok(workspace),
SkillTargetScope::Global => home.context("global skill mutations require a home directory"),
}
}
/// Return whether `path` is an existing real directory, rejecting links and
/// non-directory components. `symlink_metadata` is intentional: following a
/// link before checking it would turn a lexical CodeWhale-owned root into an
/// attacker-selected write target.
fn checked_real_directory(path: &Path) -> Result<bool> {
match fs::symlink_metadata(path) {
Ok(meta) if meta.file_type().is_symlink() => {
bail!(
"refusing to mutate symlinked Codewhale skills path component {}",
path.display()
)
}
Ok(meta) if !meta.is_dir() => bail!(
"refusing to mutate through non-directory Codewhale skills path component {}",
path.display()
),
Ok(_) => Ok(true),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
Err(err) => Err(err).with_context(|| format!("failed to inspect {}", path.display())),
}
}
/// Validate the complete owned-root chain without following a symlink in the
/// workspace/home anchor, `.codewhale`, or `skills` component.
fn validate_owned_target_chain(
anchor: &Path,View on GitHub (pinned to 73e0f67d83)