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

  1. Remove the symlink and use a real directory path (or point config at the symlink target itself)
  2. 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
  3. Avoid ever creating the pet directory with sudo; let the application `create_dir_all` it as your user
  4. 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

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


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)