Hmbown/CodeWhale · error
audited skill root identity changed; refusing mutation
Error message
audited skill root identity changed; refusing mutation
What it means
Before mutating a skill (update/remove/trust), the library re-resolves the skill and checks that the root it currently lives under still matches the root recorded in the audited skill ID. If the root identity differs, the audit is stale — the skill may have been moved, re-imported, or its catalog entry replaced — so mutation is refused rather than acting on unverified content.
Solutions
- Re-scan/re-list the skill catalog to get a fresh AuditedSkill, then retry the mutation with the fresh handle
- Check whether the skill was moved between project and global roots and target the root it actually lives in now
- If the root directory was deleted and re-created, re-import the skill so the audit records the new root identity
- Remove the stale skill and re-add it instead of updating in place
Example fix
// before
let audited = cached_skill; // stale handle from an earlier scan
mutation::remove_skill(&audited, &ctx)?; // may fail: root changed
// after
let audited = catalog.list_owned_skills()?.into_iter()
.find(|s| s.id.canonical_name == "my-skill")
.context("skill not found")?;
mutation::remove_skill(&audited, &ctx)?; // fresh audit matches root Defensive patterns
Strategy: validation
Validate before calling
fn audited_root_is_fresh(skill: &AuditedSkill) -> bool {
skill.id.root_id == skill.root.id
}
// only call update/remove/trust when this returns true; otherwise re-scan the catalog Try / catch
match mutation::remove_skill(&audited, &path, &ctx) {
Err(e) if e.to_string().contains("root identity changed") => {
let fresh = re_scan_catalog_for(&audited.id.canonical_name)?;
mutation::remove_skill(&fresh, &path, &ctx)?
}
other => other?,
} Prevention
- Always obtain AuditedSkill handles from a fresh catalog scan immediately before mutating
- Avoid caching skill handles across operations that can move or re-import skills
- Re-scan whenever the skills directories change on disk
When it happens
Trigger: Calling update_skill, remove_skill, or trust_skill with an AuditedSkill whose id.root_id no longer equals skill.root.id — e.g. the skill was moved between project and global roots, the root directory was re-created with a different identity, or a stale AuditedSkill was cached from a previous session and the catalog changed underneath it.
Common situations: A skill was imported into the global root while the app held an audited reference to the project root (or vice versa); the user deleted and re-created the .codewhale/skills directory; a long-lived session used a skill snapshot taken before the user rearranged roots manually.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- audited owned root does not match mutation target
- audited skill path does not match owned package
- owned skill path does not exist
- refusing to copy symlink
- refusing to import unsafe external skill package
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/fa4cc8aae7238dfd.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/skills/mutation.rs:333
let canonical_child = fs::canonicalize(child)
.with_context(|| format!("failed to resolve owned skill path {}", child.display()))?;
if canonical_child.parent() != Some(canonical_root.as_path()) {
bail!(
"owned skill path {} escapes direct root {}",
child.display(),
skills_dir.display()
);
}
Ok(true)
}
fn validate_owned_skill_path(
ctx: &MutationContext<'_>,
skill: &AuditedSkill,
path: &Path,
) -> Result<PathBuf> {
if skill.id.root_id != skill.root.id {
bail!("audited skill root identity changed; refusing mutation");
}
let skills_dir = validate_owned_root_descriptor(ctx, &skill.root)?;
let package_name = on_disk_package_name(&skill.id)?;
let expected = skills_dir.join(package_name);
if path != expected {
bail!(
"audited skill path {} does not match owned package {}",
path.display(),
expected.display()
);
}
validate_owned_child(&skills_dir, path, true)?;
Ok(skills_dir)
}
/// Resolve the on-disk CodeWhale-owned skills directory for a target scope.
pub fn resolve_owned_target(
workspace: &Path,View on GitHub (pinned to 73e0f67d83)