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
- Delete `state_root/<key>/journal.json` (and the whole `<key>` state dir) and rerun reconcile to rebuild the journal from a fresh snapshot.
- Restore the journal from a known-good backup or re-sync the state directory.
- Never hand-edit `journal.json`; regenerate it via reconcile.
- 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
- Never hand-edit journal.json
- Treat crash-truncated journals as corrupt: reset the state dir
- Verify disk health if journal files are repeatedly damaged
- Avoid syncing state_root between machines
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
- Recovery journal conflicts with verified content
- 不支持此分享的数据格式。
- 整理结果格式不完整
- Concurrent recovery change
- External runtime change prevents recovery
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)