Hmbown/CodeWhale · error · anyhow::Error

Codewhale credentials path must be a directory

Error message

Codewhale credentials path must be a directory

What it means

After pinning the final descriptor (every component opened with O_DIRECTORY | O_NOFOLLOW), the unix opener verifies the descriptor really is a directory. A non-directory normally fails earlier with an ENOTDIR io error at openat time, so this ensure is the fail-closed belt for exotic filesystems or a race that swaps a component between open and fstat.

Source

Thrown at crates/config/src/xai_credentials.rs:536

        }
        if fd < 0 {
            return Err(std::io::Error::last_os_error()).with_context(|| {
                format!(
                    "opening Codewhale credentials directory without following links: {}",
                    crate::quote_os_path(directory)
                )
            });
        }
        // SAFETY: `fd` is a newly owned descriptor on the success path above.
        current = unsafe { File::from_raw_fd(fd) };
    }
    let metadata = current.metadata().with_context(|| {
        format!(
            "inspecting Codewhale credentials directory {}",
            crate::quote_os_path(directory)
        )
    })?;
    anyhow::ensure!(
        metadata.is_dir(),
        "Codewhale credentials path must be a directory"
    );
    anyhow::ensure!(
        metadata.uid() == unsafe { libc::geteuid() },
        "Codewhale credentials directory must be owned by the current user"
    );
    current
        .set_permissions(fs::Permissions::from_mode(0o700))
        .with_context(|| {
            format!(
                "securing Codewhale credentials directory {}",
                crate::quote_os_path(directory)
            )
        })?;
    Ok(XaiOAuthCredentialStore {
        directory: directory.to_path_buf(),
        directory_handle: current,

View on GitHub (pinned to 8880682c63)

Solutions

  1. Replace the offending file with a real directory: remove it, then let `codewhale auth xai-device` recreate $CODEWHALE_HOME/credentials
  2. Verify $CODEWHALE_HOME sits on a normal local filesystem
  3. Re-run once if another process was moving home components concurrently

Example fix

# before: $CODEWHALE_HOME/credentials is a plain file
file "$CODEWHALE_HOME/credentials"   # ASCII text

# after
rm "$CODEWHALE_HOME/credentials"
codewhale auth xai-device   # store recreates it as a directory
Defensive patterns

Strategy: validation

Validate before calling

match std::fs::metadata(&dir) {
    Ok(m) if m.is_dir() => Ok(()),
    Ok(_) => anyhow::bail!("credentials path exists but is not a directory"),
    Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), // store will create it
    Err(e) => Err(e.into()),
}

Try / catch

match codewhale_config::with_xai_oauth_lifecycle_lock(op) {
    Ok(v) => v,
    Err(e) if e.to_string().contains("must be a directory") => {
        // remove the wrong-typed path and let the store recreate it
        let _ = std::fs::remove_file(&dir);
        codewhale_config::with_xai_oauth_lifecycle_lock(op)?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: $CODEWHALE_HOME/credentials exists as a regular file or FIFO (usually surfacing earlier as the ENOTDR open error instead); a FUSE/odd filesystem that permits O_DIRECTORY opens on non-directories; a concurrent process replacing a home-directory component mid-open.

Common situations: A leftover file named `credentials` in the home directory; home directories on unusual network or FUSE mounts; interrupted home-directory migrations.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/380bc03e7241bf00. Report an issue: GitHub.