zeroclaw-labs/zeroclaw · error

Hex string contains non-ASCII characters

Error message

Hex string contains non-ASCII characters

What it means

Second guard in hex_decode: after the even-length check, the input must be pure ASCII, because valid hex is always ASCII and this also guarantees every byte is a UTF-8 char boundary so the byte-slicing loop cannot panic on corrupt material — it returns this Err instead. It fires on the same surfaces as the odd-length error (key file, enc/enc2 ciphertext) when the string contains multi-byte characters, smart quotes, whitespace-like Unicode, or binary noise pasted into the hex field.

Source

Thrown at crates/zeroclaw-config/src/secrets.rs:977

fn build_windows_icacls_grant_arg(username: &str) -> Option<String> {
    let normalized = username.trim();
    if normalized.is_empty() {
        return None;
    }
    Some(format!("{normalized}:F"))
}

/// Hex-decode a hex string to bytes.
#[allow(clippy::manual_is_multiple_of)]
fn hex_decode(hex: &str) -> Result<Vec<u8>> {
    if (hex.len() & 1) != 0 {
        anyhow::bail!("Hex string has odd length");
    }
    // Reject non-ASCII up front: valid hex is always ASCII, and this guarantees
    // every byte is a char boundary so the byte-index slicing below cannot panic
    // on a corrupt/tampered ciphertext (it returns the Err the signature promises).
    if !hex.is_ascii() {
        anyhow::bail!("Hex string contains non-ASCII characters");
    }
    (0..hex.len())
        .step_by(2)
        .map(|i| {
            u8::from_str_radix(&hex[i..i + 2], 16)
                .map_err(|e| anyhow::Error::msg(format!("Invalid hex at position {i}: {e}")))
        })
        .collect()
}

fn is_onepassword_ref(value: &str) -> bool {
    value.starts_with("op://")
}

fn validate_onepassword_ref(reference: &str) -> Result<()> {
    let path = reference.strip_prefix("op://").unwrap_or("");
    let mut segments = path.split('/');
    let has_required_segments = (0..3).all(|_| segments.next().is_some_and(|s| !s.is_empty()));

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Find the offending bytes: `grep -P '[^\x00-\x7F]' keyfile` or open in a hex editor and remove BOM/smart quotes
  2. Restore the material from a clean copy (backup, .bak config, password manager) rather than hand-repairing characters
  3. If unrecoverable, regenerate the key (quickstart) and re-encrypt secrets — old ciphertexts will not decrypt
  4. Write keys/ciphertexts only via the tool itself; avoid round-tripping through editors/chat
Defensive patterns

Strategy: validation

Validate before calling

fn hex_ascii_ok(s: &str) -> bool {
    s.is_ascii()
}

Try / catch

let hex = std::fs::read_to_string(&key_path)?;
if !hex_ascii_ok(hex.trim()) {
    eprintln!("key file contains non-ASCII characters (BOM/smart quotes?) — restore a clean copy");
    return;
}

Prevention

When it happens

Trigger: A key file or encrypted config value containing non-ASCII: pasted from a chat/browser that converted characters (smart quotes around the hex), UTF-8 BOM prefix, mojibake after an encoding-changing transfer (Windows-1252 ↔ UTF-8), or tampered/corrupt ciphertext containing arbitrary bytes in a non-hex range.

Common situations: Copying keys through rich-text mediums that mangle characters; files saved with BOM by Windows editors; tampered secret values (the guard also serves tamper-evidence by refusing cleanly); double-encoding mishaps where raw bytes instead of hex were stored.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/e902bbfc398b56dc. Report an issue: GitHub.