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
- Replace the symlink with a real directory (mkdir the target and move contents, then remove the link)
- Remove any non-directory entry at that path and let the manager create the directory itself
- 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
- Never symlink directories inside the app's data directory; use copies or configure the data root instead
- Verify no sync/cloud tools replace data-dir entries with links
- Check for leftover files at directory paths after failed installs
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
- CodewhalePet/1
- Session goal must be a regular file
- built-in plugin path may not be a symbolic link or reparse…
- could not securely open
- external credential path must name a non-reparse regular…
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)