Hmbown/CodeWhale · error · std::io::Error
legacy spillover ownership sidecar must not be a symlink
Error message
legacy spillover ownership sidecar must not be a symlink
What it means
PermissionDenied returned by read_legacy_spillover_ownership when the ownership sidecar file is a symbolic link. Spillover payloads are content-addressed and ownership sidecars are expected to be regular files written atomically; a symlink could redirect the trusted read to arbitrary targets, so the reader refuses to follow it. This is a tamper/safety guard, not a normal runtime condition.
Source
Thrown at crates/tui/src/tools/truncate.rs:166
digest: crate::hashing::sha256_hex(bytes),
size_bytes: bytes.len().try_into().unwrap_or(u64::MAX),
};
let sidecar = legacy_spillover_ownership_path(payload_path);
let encoded = serde_json::to_vec_pretty(&ownership)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
crate::utils::write_atomic(&sidecar, &encoded)?;
Ok(sidecar)
}
pub(crate) fn read_legacy_spillover_ownership(
payload_path: &Path,
) -> io::Result<LegacySpilloverOwnership> {
let sidecar = legacy_spillover_ownership_path(payload_path);
if std::fs::symlink_metadata(&sidecar)?
.file_type()
.is_symlink()
{
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"legacy spillover ownership sidecar must not be a symlink",
));
}
let ownership = serde_json::from_slice::<LegacySpilloverOwnership>(&std::fs::read(sidecar)?)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if ownership.schema_version != LEGACY_SPILLOVER_OWNER_SCHEMA_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"unsupported legacy spillover ownership schema",
));
}
Ok(ownership)
}
/// Resolve the spillover-file path for a SHA256 content hash. Separate
/// namespace (`sha_<hex>.txt`) from the tool-call-id files so legacy
/// SHA-addressed evidence can be recognized without colliding withView on GitHub (pinned to 0c42157ee5)
Solutions
- Inspect the symlink target: `ls -l <sidecar>` and decide whether the target is trusted
- Delete the symlink and re-publish ownership (or delete the spillover payload) so a regular sidecar file is rewritten
- If you did not create the symlink, treat the machine's local state as suspect and audit who wrote it
- Exclude the Codewhale state/spillover directory from symlink-making dotfile managers
Example fix
# before $ ls -l ~/.codewhale/spillover/sha_abc.ownership ... -> /etc/passwd # read -> PermissionDenied # after $ rm ~/.codewhale/spillover/sha_abc.ownership # re-trigger the tool output write so the sidecar is recreated as a regular file
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check the sidecar with symlink_metadata (never follows links) before reading.
let meta = std::fs::symlink_metadata(&sidecar)?;
if meta.file_type().is_symlink() {
// refuse + alert; do not follow or delete automatically in security-sensitive contexts
} Type guard
fn is_symlink_sidecar(e: &std::io::Error) -> bool {
e.kind() == std::io::ErrorKind::PermissionDenied && e.to_string().contains("must not be a symlink")
} Try / catch
match read_legacy_spillover_ownership(&payload) {
Ok(o) => Ok(Some(o)),
Err(e) if is_symlink_sidecar(&e) => {
security_log("spillover sidecar replaced by symlink", payload);
Ok(None) // treat artifact as unavailable; do not silently recover
}
Err(e) => Err(e),
} Prevention
- Exclude Codewhale state/spillover directories from dotfile managers and sync tools that create symlinks
- Audit unexpected PermissionDenied reads from state dirs as potential tampering
- Never 'fix' this by replacing the symlink target read — republish the sidecar instead
When it happens
Trigger: Something replaced <payload>.ownership (the sidecar next to the spillover file) with a symlink — an attacker preparing a redirection, or an over-eager dotfile/sync manager converting files to symlinks into cloud storage.
Common situations: Home-directory sync tools (dotfile managers, network-home setups) that symlink cache/state directories; security tooling flagging the behavior after the fact; deliberately constructed repro attempts; malicious local user tampering with another user's spillover dir.
Related errors
- config path must not be a symlink: {}
- sub-agent transcript artifact is not a regular file: {}
- sub-agent state path must not traverse symlinks: {}
- sub-agent state path must be a regular file: {}
- refusing non-regular or reparse-point config lock at {}
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/dc08a65fd103e240.
Report an issue: GitHub.