Hmbown/CodeWhale · error · io::Error (InvalidInput)

export output must be outside the session store

Error message

export output must be outside the session store

What it means

The export refuses to write the archive inside the session store directory. After canonicalizing both the output's parent directory and the sessions dir, if the parent is the store itself or a subdirectory of it, the export would write into the data it is exporting (risking recursion, self-inclusion, or clobbering sessions), so it fails with InvalidInput.

Solutions

  1. Choose an output path outside the sessions directory (e.g. a dedicated `exports/` folder).
  2. If the sessions dir is configured wrongly, fix the sessions-dir setting so it does not equal the export destination's parent.
  3. In scripts, build the output path from a distinct variable, e.g. `"$HOME/exports/session-$ID.tar.xz"`.
  4. Verify with canonical paths — compare `std::fs::canonicalize`d parents — since symlinks can silently place the output inside the store.

Example fix

// before
let output = sessions_dir.join("session-123.tar.xz"); // inside the store
// after
let output = Path::new("/tmp").join("session-123.tar.xz");
Defensive patterns

Strategy: validation

Validate before calling

fn output_inside_store(output: &Path, sessions_dir: &Path) -> std::io::Result<bool> {
    let parent = output.parent().unwrap_or(Path::new(".")).canonicalize()?;
    let root = sessions_dir.canonicalize()?;
    Ok(parent.starts_with(root))
}
// if output_inside_store(out, store)? { pick another output path }

Try / catch

match write_session_archive(&session, dir, out, opts) {
    Err(e) if e.to_string().contains("outside the session store") => {
        eprintln!("choose an --output path outside {}", dir.display());
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `write_session_archive` (or `run_sessions_export`) with `output` whose canonicalized parent path starts with the canonicalized sessions_dir — e.g. exporting to `~/.codewhale/sessions/export.tar.xz` while the store is `~/.codewhale/sessions`.

Common situations: Pointing the CLI `--output` at the sessions directory by mistake; scripting an export using the same base path variable for both store and output; a config where the export dir was set equal to the sessions dir.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/b8b1231a14f697fc. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/session_export.rs:172

            io::ErrorKind::InvalidInput,
            format!(
                "xz compression level {} is out of range 0-9",
                options.compression_level
            ),
        ));
    }
    let session_json = session_json(session)?;
    let container_json = container_json(session)?;
    let parent = output
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
        .map_or_else(|| PathBuf::from("."), Path::to_path_buf);
    fs::create_dir_all(&parent)?;
    let sessions_dir = sessions_dir.map(Path::canonicalize).transpose()?;
    if let Some(root) = &sessions_dir
        && parent.canonicalize()?.starts_with(root)
    {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "export output must be outside the session store",
        ));
    }
    let artifact_files = match (options.include_artifacts, sessions_dir.as_deref()) {
        (true, Some(root)) => match session_artifacts_dir(root, &session.metadata.id)? {
            Some(dir) => collect_artifact_files(root, &dir)?,
            None => Vec::new(),
        },
        _ => Vec::new(),
    };
    let mut temp = tempfile::Builder::new()
        .prefix(".codewhale-session-export-")
        .tempfile_in(&parent)?;

    let mut summary = SessionArchiveSummary {
        output: output.to_path_buf(),
        session_id: session.metadata.id.clone(),

View on GitHub (pinned to 433685b202)