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

  1. Inspect the hex input: `wc -c` the key file — a 32-byte key must be exactly 64 hex chars; find where it got truncated
  2. 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
  3. For a corrupt config value, restore config.toml from the .bak file or version control and re-save secrets
  4. 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

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


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