BigPizzaV3/CodexPlusPlus · error

Invalid candidate hash

Error message

Invalid candidate hash

What it means

`recovery_material` reads `journal.json` and requires `candidate_sha` to be exactly 64 ASCII hex characters (a SHA-256 hex string), because that value is interpolated into the candidate filename `candidate-<sha>.mjs`. A malformed hash means the journal is corrupt, hand-edited, or from an incompatible schema, and using it could point at a wrong or attacker-controlled filename.

Solutions

  1. Delete `state_root/<key>/journal.json` (and the whole `<key>` state dir) and rerun reconcile to rebuild the journal from a fresh snapshot.
  2. Restore the journal from a known-good backup or re-sync the state directory.
  3. Never hand-edit `journal.json`; regenerate it via reconcile.
  4. Check disk health if journal files are repeatedly truncated.

Example fix

// before (journal.json)
{"schema":1,"candidate_sha":"abc123",...}
// after
# delete corrupt journal and regenerate
rm -rf ~/.codex/plugins/state/<key> && codex reconcile --browser-enabled
Defensive patterns

Strategy: validation

Validate before calling

fn valid_sha_hex(s: &str) -> bool {
    s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
}
let journal: serde_json::Value = serde_json::from_slice(&std::fs::read(state_root.join(&key).join("journal.json"))?)?;
if !valid_sha_hex(journal["candidate_sha"].as_str().unwrap_or("")) {
    // journal corrupt: reset state_root/<key> before reconcile
}

Type guard

fn journal_candidate_sha(j: &serde_json::Value) -> Option<&str> {
    let s = j.get("candidate_sha")?.as_str()?;
    if s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()) { Some(s) } else { None }
}

Try / catch

match reconcile(&paths, true) {
    Err(e) if e.to_string().contains("Invalid candidate hash") => {
        std::fs::remove_dir_all(state_root.join(&key))?;
        reconcile(&paths, true)?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Any caller of `recovery_material` (`prepare`, `restore_all`, `verify_restored_state`) loads a `journal.json` whose `candidate_sha` fails the length/hex check — e.g. truncated file, JSON edited manually, or a journal written by a different tool.

Common situations: Journal partially written/corrupted by a crash or disk issue; user edited `journal.json` while debugging; state directory synced between machines with divergent journal versions.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/88257bb4bf2e3ce6. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/native_browser.rs:470

    );
    atomic_write(&target, &candidate)?;
    ensure!(
        read_regular(&target, MAX_SERVICE)? == candidate,
        "Runtime write verification failed"
    );
    Ok(())
}

fn recovery_material(
    paths: &BrowserPaths,
    key: &str,
    contract: &RuntimeContract,
) -> Result<(Journal, Vec<u8>, Vec<u8>)> {
    ensure!(key_valid(key), "Invalid recovery key");
    let dir = paths.state_root.join(key);
    let journal: Journal = serde_json::from_slice(&read_regular(&dir.join("journal.json"), 4096)?)?;
    let original = read_regular(&dir.join("original.mjs"), MAX_SERVICE)?;
    ensure!(
        journal.candidate_sha.len() == 64
            && journal.candidate_sha.bytes().all(|b| b.is_ascii_hexdigit()),
        "Invalid candidate hash"
    );
    let candidate = read_regular(
        &dir.join(format!("candidate-{}.mjs", journal.candidate_sha)),
        MAX_SERVICE,
    )?;
    ensure!(
        journal.schema == 1
            && (journal.original_sha == contract.service_sha
                || journal.original_sha == ORIGINAL_SHA)
            && sha(&original) == journal.original_sha
            && journal.candidate_sha == sha(&candidate)
            && journal.modified_nanos < 1_000_000_000,
        "Recovery journal conflicts with verified content"
    );
    Ok((journal, original, candidate))

View on GitHub (pinned to b1ed92e5e4)