Hmbown/CodeWhale · error
Pet directory must belong to this user and cannot be a link
Error message
Pet directory must belong to this user and cannot be a link
What it means
When the pet-watch owner server starts (`serve`), it creates the pet directory and then verifies via `symlink_metadata` that the path is a real directory owned by the current effective UID. If the path is a symlink, another file type, or owned by a different user, it bails with this error and forces permissions to 0700 on success. This prevents another local user from pre-planting a symlink or directory that this process would write into (a local privilege/tampering guard).
Solutions
- Remove the symlink and use a real directory path (or point config at the symlink target itself)
- Chown the directory to your user: `chown -R "$(id -u):$(id -g)" <pet-dir>` (with appropriate privileges), or delete it and let the server recreate it
- Avoid ever creating the pet directory with sudo; let the application `create_dir_all` it as your user
- Verify with `ls -ld <pet-dir>` that it is a `d...` entry owned by your UID before starting the server
Example fix
// before # pets is a symlink to /mnt/data/pets, owned by root ln -s /mnt/data/pets ~/pets // after rm ~/pets mkdir -p /mnt/data/pets && chown "$(id -u)" /mnt/data/pets # or simply: mkdir -p ~/pets (real dir, owned by you)
Defensive patterns
Strategy: validation
Validate before calling
use std::os::unix::fs::MetadataExt;
let md = std::fs::symlink_metadata(&pet_root)?;
let safe = md.is_dir() && md.uid() == unsafe { libc::geteuid() };
if !safe {
// fix ownership/symlink before calling serve
} Type guard
fn pet_dir_is_safe(path: &std::path::Path) -> bool {
use std::os::unix::fs::MetadataExt;
std::fs::symlink_metadata(path)
.map(|md| md.is_dir() && md.uid() == unsafe { libc::geteuid() })
.unwrap_or(false)
} Try / catch
match serve(&pet_root) {
Err(e) if e.to_string().contains("must belong to this user") => {
eprintln!("Fix the pet directory: real dir, owned by your UID (chown / remove symlink)");
}
other => other?,
} Prevention
- Never create the pet directory with sudo; let the app create it as your user
- Avoid symlinks in the configured pet root path
- Check `ls -ld` ownership before first run on shared machines
- If a stale root-owned directory exists, remove it and let the app recreate it
When it happens
Trigger: The configured pet `root` path is a symlink; the directory exists but is owned by another user (e.g. created by root or a different account); the path was pre-created by an installer running as a different UID.
Common situations: Pointing the pet directory at a symlinked path like `~/pets -> /mnt/data/pets`; running once with `sudo` so the directory is root-owned; shared machines where a stale directory from an old account remains.
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
- Codewhale credentials directory must be owned by the…
- Codewhale-owned xAI OAuth storage owner is not the current…
- xAI OAuth file must be owned by the current user
- agent action=claim widens an enforced write scope, and the…
- append_allow_rules only accepts action = "allow"
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/f735e0433622175b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/tui/pet_watch/owner.rs:292
],
Json(json!({"identity":state.descriptor.identity})),
)
.into_response()
}
/// Starts only on an explicit pet command or when a view first attaches.
/// The lifetime lock is held across HTTP serving, ticks and all checkpoints.
// `pet serve` is its own console process, never inside the alt-screen: its
// startup line and stop reason are the operator's only output.
#[allow(clippy::print_stdout, clippy::print_stderr)]
pub fn serve(root: PathBuf, requested_port: u16) -> anyhow::Result<()> {
std::fs::create_dir_all(&root)?;
#[cfg(unix)]
{
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = std::fs::symlink_metadata(&root)?;
if !metadata.is_dir() || metadata.uid() != unsafe { libc::geteuid() } {
anyhow::bail!("Pet directory must belong to this user and cannot be a link");
}
std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700))?;
}
let lock_path = WorkspaceFile::open(&root, Path::new("owner.lock"), true)?;
let original = lock_path.open_update(true, false)?;
let mut lifetime = fd_lock::RwLock::new(original.try_clone()?);
let _guard = lifetime
.try_write()
.map_err(|_| anyhow::anyhow!("Another pet owner is running"))?;
let mut store = Store::at(&root)?;
let previous = store.load()?;
let mut saved = if let Some(text) = previous {
let value: Saved = serde_json::from_str(&text)?;
if value.version != 1
|| uuid::Uuid::parse_str(&value.identity).is_err()
|| value.token.len() != 64
|| !value.token.bytes().all(|b| b.is_ascii_hexdigit())
|| !valid_source(&value.source)View on GitHub (pinned to 73e0f67d83)