Hmbown/CodeWhale · error · anyhow::Error

Runtime store directory must not be a symlink: {}

Error message

Runtime store directory must not be a symlink: {}

What it means

reject_symlinked_store_dir (called by ensure_runtime_store_dir after create_dir_all) bails when the store directory itself is a symlink (crates/tui/src/runtime_threads.rs:9032). Directory-level symlinks can be retargeted between the existence check and writes, breaking the guarantee that events and turns land in one canonical, append-only location.

Source

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

    std::thread::sleep(EVENT_TRANSACTION_LOCK_POLL.min(timeout - elapsed));
    Ok(())
}

fn rollback_failed_event_append_handle(rollback_file: &File, original_len: u64) -> Result<()> {
    rollback_file
        .set_len(original_len)
        .context("Failed to roll back Runtime event")?;
    rollback_file
        .sync_all()
        .context("Failed to sync Runtime event rollback")
}

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

fn ensure_runtime_store_dir(path: &Path) -> Result<()> {
    fs::create_dir_all(path).with_context(|| format!("Failed to create {}", path.display()))?;
    reject_symlinked_store_dir(path)
}

fn read_complete_event(
    reader: &mut impl BufRead,
    path: &Path,

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Replace the symlink with a real directory and move the actual data there
  2. Configure the store root to point directly at the real target directory
  3. Use a bind mount (Linux) or the platform's supported volume mechanism instead of a symlink
  4. Remove the link, let create_dir_all make a real directory, then migrate contents

Example fix

# before
$ rm -rf ~/.codewhale/runtime && ln -s /data/codewhale ~/.codewhale/runtime

# after
$ rm ~/.codewhale/runtime
$ mkdir -p ~/.codewhale/runtime
$ cp -a /data/codewhale/. ~/.codewhale/runtime/
# or: point the store-root setting at /data/codewhale directly
Defensive patterns

Strategy: validation

Validate before calling

// Validate the store directory shape before runtime start.
let md = std::fs::symlink_metadata(&store_dir)
    .context("store dir metadata")?;
if md.file_type().is_symlink() {
    return Err(anyhow::anyhow!("store dir is a symlink"));
}
if !md.is_dir() {
    return Err(anyhow::anyhow!("store path is not a directory"));
}

Type guard

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

Try / catch

match RuntimeStore::open(store_dir) {
    Ok(store) => store,
    Err(err) if err.to_string().contains("must not be a symlink") => {
        // Replace the link with a real dir, then retry once.
        replace_symlink_with_dir(&store_dir)?;
        RuntimeStore::open(store_dir)?
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: User symlinks ~/.codewhale/runtime (or equivalent) to another volume, a synced folder, or a tmpfs; dotfile managers linking state directories; container images linking the store to a volume path

Common situations: Moving state to a bigger disk via symlink; Docker setups linking store dirs; Nix/home-manager configs that manage state dirs as symlinks.

Related errors


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