Hmbown/CodeWhale · error · io::Error (InvalidData)
artifact name is not portable UTF-8
Error message
artifact name is not portable UTF-8
What it means
Every artifact entry name must be valid UTF-8 and free of Windows-hostile characters (backslash and colon) so that names are portable in the archive and safe as archive member paths. A file whose name is non-UTF-8 (common on Linux with arbitrary bytes in filenames) or contains `\` / `:` is rejected with InvalidData rather than producing a corrupt or non-portable archive.
Solutions
- Rename the offending files to portable ASCII names (`mv` the files detected with `convmv` or a find command).
- Fix the tool writing artifacts to use UTF-8 names without `:` or `\` (e.g. use `-` instead of `:` in timestamps).
- Remove the offending files if they are disposable.
- On macOS/Windows this is rare; on Linux, run `find <dir> -name *\:* -o -name *\\*` and also check for invalid-UTF-8 names with `find <dir> | grep -P '[^\x00-\x7F]'` before exporting.
Example fix
// before
// artifacts/log:2024-01-01.txt (colon in name) -> export fails
// after
fs::rename("artifacts/log:2024-01-01.txt", "artifacts/log-2024-01-01.txt")?; Defensive patterns
Strategy: validation
Validate before calling
fn portable_name(name: &std::ffi::OsStr) -> bool {
name.to_str().map(|s| !s.contains(['\\', ':'])).unwrap_or(false)
}
// scan artifacts: reject any entry whose file_name() fails portable_name Type guard
fn as_portable_name(name: &std::ffi::OsStr) -> Option<&str> {
name.to_str().filter(|s| !s.contains(['\\', ':']))
} Try / catch
match write_session_archive(&session, dir, out, opts) {
Err(e) if e.to_string().contains("not portable UTF-8") => {
eprintln!("rename non-portable artifact filenames before export");
}
other => other?,
} Prevention
- Generate artifact filenames as ASCII/UTF-8 without ':' and '\\' (use '-' in timestamps)
- Rename files transferred from Windows/other encodings before placing them in artifacts
- Scan the artifacts dir with a portable-name check before exporting on Linux
When it happens
Trigger: The artifacts directory contains a file created with non-UTF-8 raw bytes in its name (e.g. from a tool that wrote locale-raw names on Linux), or a name containing `\` or `:`.
Common situations: Files copied from a FAT/NTFS volume or Windows machine onto a Linux artifacts dir; scripts generating names with timestamps containing `:`; archives extracted with encoding-raw filenames.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- read-only executable path is not valid UTF-8
- already exists; pass --force to overwrite it
- artifact tree exceeds export depth limit
- artifact tree exceeds export entry limit
- Codewhale-owned xAI OAuth file
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/10b3b706c29c32b8.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/session_export.rs:362
for entry in fs::read_dir(dir)? {
let entry = entry?;
*entries += 1;
if *entries > MAX_ARTIFACT_ENTRIES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"artifact tree exceeds export entry limit",
));
}
let file_type = entry.file_type()?;
if file_type.is_symlink() {
continue;
}
let child = entry.file_name();
let child = child
.to_str()
.filter(|name| !name.contains(['\\', ':']))
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"artifact name is not portable UTF-8",
)
})?;
let member = format!("{prefix}/{child}");
let path = entry.path();
let relative = path.strip_prefix(sessions_dir).map_err(io::Error::other)?;
if file_type.is_dir() {
// Reject substituted parent directories before descending. The file
// read repeats confinement checks, so directory races cannot leak bytes.
WorkspaceFile::open(sessions_dir, &relative.join(".export-root-check"), false)?;
collect_artifact_files_recursive(
sessions_dir,
&path,
&member,
depth + 1,
entries,
files,View on GitHub (pinned to 433685b202)