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
- Pass the extension without the leading dot and using only ASCII letters/digits (e.g. "png", "json").
- Ensure the artifact id is a non-empty safe-ASCII identifier before calling.
- Validate/normalize inputs at the call site: strip dots, reject or slugify non-alphanumeric characters.
- 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
- Strip the leading dot from filename extensions before passing.
- Default to a safe extension (e.g. "txt") when the source has none.
- Filter extensions to ASCII alphanumerics at the boundary.
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
- evidence metadata exceeds limit
- Fleet artifact exceeds the 16 MiB limit
- invalid-channel
- Invalid durable task id
- Invalid pet voice.
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 explicitView on GitHub (pinned to 73e0f67d83)