zeroclaw-labs/zeroclaw · error
Hex string has odd length
Error message
Hex string has odd length
What it means
hex_decode is the parser for hex-encoded material in zeroclaw-config's secret store — the master key file contents and `enc:`/`enc2:` ciphertext bodies. It bails when the input length is odd, because hex encodes one byte per two characters, so an odd length can never decode. In practice this means the stored material is corrupt or truncated: the key file was cut mid-write, edited by hand, or the config value was mangled. Callers wrap it with context like "Failed to read secret key file" or "Secret key file created by concurrent process is corrupt".
Source
Thrown at crates/zeroclaw-config/src/secrets.rs:971
s
}
/// Build the `/grant` argument for `icacls` using a normalized username.
/// Returns `None` when the username is empty or whitespace-only.
#[cfg(any(windows, test))]
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://")View on GitHub (pinned to 88bb9c8533)
Solutions
- Inspect the hex input: `wc -c` the key file — a 32-byte key must be exactly 64 hex chars; find where it got truncated
- If the key file is corrupt and unrecoverable, regenerate it (delete ~/.zeroclaw/.secret_key and re-run `zeroclaw quickstart`) — note values encrypted with the old key become undecryptable
- For a corrupt config value, restore config.toml from the .bak file or version control and re-save secrets
- Always copy key material programmatically, never by selecting text
Defensive patterns
Strategy: validation
Validate before calling
fn hex_shape_ok(s: &str) -> bool {
s.len() % 2 == 0
} Try / catch
match std::fs::read_to_string(&key_path) {
Ok(hex) if !hex_shape_ok(hex.trim()) => {
eprintln!("key file at {} has an odd hex length — it is truncated; restore from backup or regenerate", key_path.display());
}
other => other?,
} Prevention
- Copy key files only with cp/rsync — never by selecting text
- Keep a durable backup of the master key (secret manager) so corruption means restore, not data loss
- After any manual edit of key/ciphertext files, check `wc -c` matches 2x the byte length (64 chars for the 32-byte key)
When it happens
Trigger: load_or_create_key reads ~/.zeroclaw/.secret_key whose hex was truncated (disk full during an old write, manual edit, copy-paste losing a char); decrypt_chacha20/decrypt_legacy_xor parse an enc2:/enc: value from config.toml that was truncated or hand-edited; a secrets value migrated between systems lost characters.
Common situations: Key file corrupted by a crash during an old non-atomic write; user hand-copied the key between machines and dropped a character; config files trimmed/sanitized by tooling that cut long strings.
Related errors
- Hex string contains non-ASCII characters
- The '{}' backend does not support automatic key generation.
- Key file path is a symlink — refusing to write
- Cannot determine current username; ACL hardening is required
- Failed to take ownership of key file via takeown; cannot est
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/f15517e582bb7ec8.
Report an issue: GitHub.