Hmbown/CodeWhale · error · io::Error
invalid approval receipt at line
Error message
invalid approval receipt at line {}: {err} What it means
While loading approval receipts from the on-disk log (`load_unlocked`, called by `load` and `append`), each line must deserialize into a receipt via serde_json. A line that fails JSON parsing or does not match the receipt schema is wrapped in an InvalidData io::Error reporting the 1-based line number and the serde error.
Solutions
- Open the file at the reported line number and fix or remove the malformed line (keep one valid JSON receipt per line).
- Back up the log, then delete corrupt trailing/partial lines (typically the last line after a crash).
- Check whether a codewhale version changed the receipt schema; regenerate or migrate old logs.
- Ensure only one process writes the log at a time (use the provided locked APIs, not raw file edits).
Example fix
// before: manually appended JSON object spanning multiple lines
// after: one compact JSON receipt per line
{"session_id":"abc","tool":"bash","decision":"allow"} Defensive patterns
Strategy: try-catch
Validate before calling
fn log_lines_valid(path: &Path) -> bool {
std::fs::read_to_string(path).map(|s| {
s.lines().enumerate().all(|(i, l)| {
serde_json::from_str::<serde_json::Value>(l)
.map_err(|e| { eprintln!("line {}: {e}", i + 1); e })
.is_ok()
})
}).unwrap_or(false)
} Try / catch
match log.load() {
Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
// parse line number from message, truncate/repair log, retry
}
other => other?,
} Prevention
- Never hand-edit approval logs; use the locked API.
- Write one compact JSON receipt per line.
- Handle partial last lines after crashes before loading.
- Migrate logs when the Receipt schema changes between versions.
When it happens
Trigger: Reading an approval log file where any line is empty, truncated (crash mid-write), hand-edited, or a valid JSON value that doesn't match the Receipt schema (missing/renamed fields).
Common situations: A crash or kill -9 during append leaving a partial line; manual editing of the log; a version change in the Receipt struct making old entries incompatible; log corrupted by two writers without locking.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- deserialize
- <dynamic: wrapped serde_json parse error>
- <dynamic: wrapped serde_json serialization error>
- <dynamic: wrapped serde_json serialization error>
- Failed to parse Ollama /api/tags JSON
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/1a1b14a3e2e5f7d0.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/approval_log.rs:248
match OpenOptions::new().read(true).open(path) {
Ok(file) => Ok(Some(file)),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}
fn load_unlocked(&self, session_id: &str) -> io::Result<Vec<ApprovalReceipt>> {
let path = self.log_path(session_id)?;
let file = match File::open(path) {
Ok(file) => file,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err),
};
let mut receipts = Vec::new();
for (index, line) in BufReader::new(file).lines().enumerate() {
let line = line?;
let receipt = serde_json::from_str(&line).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid approval receipt at line {}: {err}", index + 1),
)
})?;
receipts.push(receipt);
}
ApprovalReplay::from_receipts(&receipts)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
Ok(receipts)
}
pub(crate) fn load(&self, session_id: &str) -> io::Result<Vec<ApprovalReceipt>> {
if !self.log_path(session_id)?.exists() {
return Ok(Vec::new());
}
let Some(lock_file) = self.open_existing_lock_file(session_id)? else {
// Imported or legacy snapshots can contain a receipt log without
// its ephemeral lock file. Preserve read-only session loading;View on GitHub (pinned to 73e0f67d83)