{"record":{"id":"f735e0433622175b","repo":"Hmbown/CodeWhale","slug":"pet-directory-must-belong-to-this-user-and-cannot-be-a-link","errorCode":null,"errorMessage":"Pet directory must belong to this user and cannot be a link","messagePattern":"Pet directory must belong to this user and cannot be a link","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tui/pet_watch/owner.rs","lineNumber":292,"sourceCode":"        ],\n        Json(json!({\"identity\":state.descriptor.identity})),\n    )\n        .into_response()\n}\n\n/// Starts only on an explicit pet command or when a view first attaches.\n/// The lifetime lock is held across HTTP serving, ticks and all checkpoints.\n// `pet serve` is its own console process, never inside the alt-screen: its\n// startup line and stop reason are the operator's only output.\n#[allow(clippy::print_stdout, clippy::print_stderr)]\npub fn serve(root: PathBuf, requested_port: u16) -> anyhow::Result<()> {\n    std::fs::create_dir_all(&root)?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::{MetadataExt, PermissionsExt};\n        let metadata = std::fs::symlink_metadata(&root)?;\n        if !metadata.is_dir() || metadata.uid() != unsafe { libc::geteuid() } {\n            anyhow::bail!(\"Pet directory must belong to this user and cannot be a link\");\n        }\n        std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700))?;\n    }\n    let lock_path = WorkspaceFile::open(&root, Path::new(\"owner.lock\"), true)?;\n    let original = lock_path.open_update(true, false)?;\n    let mut lifetime = fd_lock::RwLock::new(original.try_clone()?);\n    let _guard = lifetime\n        .try_write()\n        .map_err(|_| anyhow::anyhow!(\"Another pet owner is running\"))?;\n    let mut store = Store::at(&root)?;\n    let previous = store.load()?;\n    let mut saved = if let Some(text) = previous {\n        let value: Saved = serde_json::from_str(&text)?;\n        if value.version != 1\n            || uuid::Uuid::parse_str(&value.identity).is_err()\n            || value.token.len() != 64\n            || !value.token.bytes().all(|b| b.is_ascii_hexdigit())\n            || !valid_source(&value.source)","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tui/pet_watch/owner.rs#L274-L310","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n# pets is a symlink to /mnt/data/pets, owned by root\nln -s /mnt/data/pets ~/pets\n// after\nrm ~/pets\nmkdir -p /mnt/data/pets && chown \"$(id -u)\" /mnt/data/pets\n# or simply: mkdir -p ~/pets  (real dir, owned by you)","handlingStrategy":"validation","validationCode":"use std::os::unix::fs::MetadataExt;\nlet md = std::fs::symlink_metadata(&pet_root)?;\nlet safe = md.is_dir() && md.uid() == unsafe { libc::geteuid() };\nif !safe {\n    // fix ownership/symlink before calling serve\n}","typeGuard":"fn pet_dir_is_safe(path: &std::path::Path) -> bool {\n    use std::os::unix::fs::MetadataExt;\n    std::fs::symlink_metadata(path)\n        .map(|md| md.is_dir() && md.uid() == unsafe { libc::geteuid() })\n        .unwrap_or(false)\n}","tryCatchPattern":"match serve(&pet_root) {\n    Err(e) if e.to_string().contains(\"must belong to this user\") => {\n        eprintln!(\"Fix the pet directory: real dir, owned by your UID (chown / remove symlink)\");\n    }\n    other => other?,\n}","preventionTips":["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"],"tags":["security","symlink","ownership","permissions","directory"],"backgroundTag":"permission-denied","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}