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
- Find the offending bytes: `grep -P '[^\x00-\x7F]' keyfile` or open in a hex editor and remove BOM/smart quotes
- Restore the material from a clean copy (backup, .bak config, password manager) rather than hand-repairing characters
- If unrecoverable, regenerate the key (quickstart) and re-encrypt secrets — old ciphertexts will not decrypt
- 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
- Save secret material as plain UTF-8/ASCII without BOM; disable 'smart quotes' in editors used for config
- Transfer keys via binary-safe channels (scp, secret manager), not chat/email/clipboard round-trips
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
- Hex string has odd length
- matrix: corruption recovery looped — aborting to avoid an in
- microsoft365.client_secret must not be empty when auth_flow
- microsoft365.client_secret must not be empty when auth_flow
- jira.api_token must be set (or JIRA_API_TOKEN env var) when
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/e902bbfc398b56dc.
Report an issue: GitHub.