Hmbown/CodeWhale · error

artifact name is not portable UTF-8

Error message

artifact name is not portable UTF-8

What it means

Artifact file names must be portable UTF-8 and must not contain backslash or colon characters. When a directory entry's name is not valid UTF-8 or contains those characters, the walk aborts with InvalidData so archives stay portable across filesystems and platforms.

Solutions

  1. Rename the offending artifact file to a portable UTF-8 name without '\\' or ':' and re-export.
  2. Identify offenders with: find <artifacts_dir> -name '*[\\:]*' and with ls | grep -P '[^\x00-\x7F]'.
  3. If files came from another platform, normalize names during copy-in rather than at export time.
  4. Fix the tool that created non-portable names so future sessions export cleanly.

Example fix

// before
artifacts/logs/'C:\temp\build.log'
// after
$ mv 'artifacts/logs/C:\temp\build.log' 'artifacts/logs/C-temp-build.log'
Defensive patterns

Strategy: validation

Validate before calling

fn portable_name(name: &std::ffi::OsStr) -> bool {
    name.to_str()
        .map(|s| !s.contains('\\') && !s.contains(':'))
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: A file or directory under artifacts/ has a non-UTF-8 (raw byte) name, or its name contains '\\' or ':' — often from files created on another OS or by tools writing raw bytes.

Common situations: Files copied from Windows with names like 'C:\temp\log.txt' baked in; files created on Linux with invalid UTF-8 byte sequences; NFC/NFD or encoding issues from network transfers.

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


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). 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 73e0f67d83)