Hmbown/CodeWhale · error · io::Error

artifact id and extension must contain safe ASCII characters

Error message

artifact id and extension must contain safe ASCII characters

What it means

Session artifacts are written under a path built from the artifact id and extension, so both must be safe ASCII. This error is returned when the artifact id is empty, the extension is empty, or the extension contains a non-alphanumeric-ASCII character, blocking path traversal and unsafe filenames.

Solutions

  1. Pass the extension without the leading dot and using only ASCII letters/digits (e.g. "png", "json").
  2. Ensure the artifact id is a non-empty safe-ASCII identifier before calling.
  3. Validate/normalize inputs at the call site: strip dots, reject or slugify non-alphanumeric characters.
  4. If the source file has no extension, choose a default extension rather than passing an empty string.

Example fix

// before
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); // may be empty
write_session_artifact_bytes(session, id, ext, bytes)?;

// after
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("txt");
let ext: String = ext.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
write_session_artifact_bytes(session, id, &ext, bytes)?;
Defensive patterns

Strategy: validation

Validate before calling

fn safe_extension(ext: &str) -> bool {
    !ext.is_empty() && ext.chars().all(|c| c.is_ascii_alphanumeric())
}

Type guard

fn normalize_ext(ext: &str) -> Option<&str> {
    let e = ext.trim_start_matches('.');
    (!e.is_empty() && e.chars().all(|c| c.is_ascii_alphanumeric())).then_some(e)
}

Try / catch

if let Err(e) = write_session_artifact_bytes(session, id, ext, bytes) {
    if e.kind() == std::io::ErrorKind::InvalidInput { /* sanitize id/ext and retry */ }
}

Prevention

When it happens

Trigger: Calling `write_session_artifact_bytes` (via `session_artifact_relative_path_with_extension`) with an empty artifact_id, an empty extension, or an extension containing characters like `.`, `/`, spaces, or unicode (e.g. passing ".png" with the dot included).

Common situations: Deriving the extension from a filename with `path.extension()` on a file with no extension; including the leading dot ('.tar.gz'); empty id from an uninitialized record; user-supplied names with special characters.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/src/artifacts.rs:84

#[must_use]
pub fn session_artifact_relative_path(artifact_id: &str) -> PathBuf {
    PathBuf::from(ARTIFACTS_DIR_NAME).join(format!("{artifact_id}.txt"))
}

fn session_artifact_relative_path_with_extension(
    artifact_id: &str,
    extension: &str,
) -> io::Result<PathBuf> {
    let artifact_id = sanitize_id_component(artifact_id);
    let extension = extension.trim_start_matches('.').to_ascii_lowercase();
    if artifact_id.is_empty()
        || extension.is_empty()
        || !extension
            .chars()
            .all(|character| character.is_ascii_alphanumeric())
    {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "artifact id and extension must contain safe ASCII characters",
        ));
    }
    Ok(PathBuf::from(ARTIFACTS_DIR_NAME).join(format!("{artifact_id}.{extension}")))
}

fn artifact_sessions_root() -> Option<PathBuf> {
    #[cfg(test)]
    if let Some(root) = TEST_ARTIFACT_SESSIONS_ROOT
        .lock()
        .unwrap_or_else(|err| err.into_inner())
        .clone()
    {
        return Some(root);
    }

    // Use the same state-root authority as saved sessions, including an explicit

View on GitHub (pinned to 73e0f67d83)