Hmbown/CodeWhale · error

Session goal store must be a real directory

Error message

Session goal store {dir} must be a real directory

What it means

The session goal store loader requires the goals directory to be a real directory on disk: it uses symlink_metadata so symlinks are detected before resolution, and rejects both symlinks and non-directory entries with InvalidData. NotFound returns Ok(None) (no store), but a symlink pointing anywhere — even to a valid directory — is refused. This prevents goals being read from or written through a symlinked path.

Solutions

  1. Replace the symlink with a real directory (mkdir the target and move contents, then remove the link)
  2. Remove any non-directory entry at that path and let the manager create the directory itself
  3. Check what is mounted at that path (mounts, bind mounts, cloud-sync agents) and ensure it is a plain local directory

Example fix

// before
ln -s ~/Dropbox/codewhale-goals ~/.local/share/codewhale/goals
// after
mkdir -p ~/.local/share/codewhale/goals  # real directory, sync via tooling not symlink
Defensive patterns

Strategy: validation

Validate before calling

let md = std::fs::symlink_metadata(&goals_dir)?;
if md.file_type().is_symlink() || !md.is_dir() { return Err(anyhow!("goals dir must be a real directory")); }

Type guard

fn is_real_dir(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p).map(|m| !m.file_type().is_symlink() && m.is_dir()).unwrap_or(false)
}

Try / catch

match manager.open_goal_store() {
    Ok(g) => use(g),
    Err(e) if e.to_string().contains("must be a real directory") => {
        eprintln!("Remove the symlink at the goals path; the app does not follow symlinks");
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling the goal-store open/read path when the directory at `dir` exists but is a symlink, a regular file, or otherwise not a real directory per symlink_metadata.

Common situations: A user symlinks the goals directory into a synced/cloud folder for convenience; a previous run left a file where the directory belongs; container/volume mounts present the path as something other than a plain directory.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/088495a2dd687b8c. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/session_manager.rs:1312

    }

    fn checkpoints_dir(&self) -> PathBuf {
        self.sessions_dir.join("checkpoints")
    }

    fn session_goals_dir(&self) -> PathBuf {
        self.sessions_dir.join(SESSION_GOALS_DIR)
    }

    fn checked_existing_session_goals_dir(&self) -> std::io::Result<Option<PathBuf>> {
        let dir = self.session_goals_dir();
        let metadata = match fs::symlink_metadata(&dir) {
            Ok(metadata) => metadata,
            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
            Err(error) => return Err(error),
        };
        if metadata.file_type().is_symlink() || !metadata.is_dir() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "Session goal store {} must be a real directory",
                    dir.display()
                ),
            ));
        }
        Ok(Some(dir))
    }

    fn ensure_session_goals_dir(&self) -> std::io::Result<PathBuf> {
        if let Some(dir) = self.checked_existing_session_goals_dir()? {
            return Ok(dir);
        }
        let dir = self.session_goals_dir();
        match fs::create_dir(&dir) {
            Ok(()) => {}
            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}

View on GitHub (pinned to 73e0f67d83)