Hmbown/CodeWhale · error · anyhow::Error

Runtime store file must not be a symlink: {}

Error message

Runtime store file must not be a symlink: {}

What it means

reject_symlinked_store_file uses symlink_metadata and bails when a store file (event log, turn record, etc.) is a symlink (crates/tui/src/runtime_threads.rs:8916). Append-heavy write paths, rollbacks, and atomic renames assume a real regular file; symlinks can swap targets mid-run, defeating durability guarantees, so they are refused.

Source

Thrown at crates/tui/src/runtime_threads.rs:8916

            Component::ParentDir => {
                normalized.pop();
            }
            Component::Normal(part) => normalized.push(part),
        }
    }
    if normalized.as_os_str().is_empty() {
        PathBuf::from(".")
    } else {
        normalized
    }
}

fn reject_symlinked_store_file(path: &Path) -> Result<()> {
    let Ok(metadata) = fs::symlink_metadata(path) else {
        return Ok(());
    };
    if metadata.file_type().is_symlink() {
        bail!(
            "Runtime store file must not be a symlink: {}",
            path.display()
        );
    }
    Ok(())
}

fn open_runtime_store_file(
    path: &Path,
    purpose: &str,
    configure: impl FnOnce(&mut OpenOptions),
) -> Result<File> {
    reject_symlinked_store_file(path)?;
    let mut options = OpenOptions::new();
    configure(&mut options);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt as _;

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Delete the symlink and restore a real file at that path (copy the data back if needed)
  2. Relocate the entire store root via configuration instead of symlinking individual files
  3. If you need the data elsewhere, use an OS-level bind mount or move the whole directory
  4. Re-run the runtime after removing links; missing files are fine, symlinks are not

Example fix

# before
$ ln -s /mnt/bigdisk/events.jsonl ~/.codewhale/runtime/events.jsonl

# after
# move the whole store instead of one file
$ mv ~/.codewhale/runtime /mnt/bigdisk/runtime
# then set the store root config to /mnt/bigdisk/runtime
Defensive patterns

Strategy: validation

Validate before calling

// Refuse to start when any store file is a symlink.
for file in store_dir.files() {
    if let Ok(md) = std::fs::symlink_metadata(&file) {
        if md.file_type().is_symlink() {
            return Err(anyhow::anyhow!("symlinked store file: {}", file.display()));
        }
    }
}

Type guard

fn is_regular_file(path: &Path) -> bool {
    std::fs::symlink_metadata(path)
        .map(|md| md.file_type().is_file())
        .unwrap_or(false)
}

Try / catch

match open_runtime_store(store_dir) {
    Ok(store) => store,
    Err(err) if err.to_string().contains("must not be a symlink") => {
        // surface to the user: they must replace links with real files
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: User symlinks an events file onto another disk, a RAM disk, or a synced folder; a dotfile/backup manager replaced store files with links; a restore script recreated files as symlinks

Common situations: Trying to move bulky event logs off a small disk; sharing one store between machines via symlinked files; migration tools that link instead of copy.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/3b5d57628cfdc928. Report an issue: GitHub.