gitbutlerapp/gitbutler · error
transcript contains malformed JSON before the final record
Error message
transcript contains malformed JSON before the final record
What it means
`TranscriptBatch::parse` splits the raw agent transcript snapshot (JSONL from Claude Code or Codex) into lines and parses each as JSON. The final line may be malformed because the agent is still appending to the file (writers append whole lines, so only the tail can be partial); any malformed line before the last one bails with this message. It therefore signals mid-file corruption, not an in-progress writer.
Source
Thrown at crates/but-agentlog/src/transcript.rs:47
session_id: None,
provider: match agent {
Agent::Codex => None,
Agent::Claude => Some("anthropic".to_string()),
},
model: None,
tool_version: None,
thread_source: None,
records: Vec::new(),
};
let mut codex_tool_names = HashMap::new();
let mut codex_spawn_prompts = HashSet::new();
let mut claude_tool_names = HashMap::new();
while let Some((index, trimmed)) = raw_records.next() {
let parsed = match serde_json::from_slice::<Value>(trimmed) {
Ok(parsed) => parsed,
Err(_) if raw_records.peek().is_none() => continue,
Err(_) => bail!("transcript contains malformed JSON before the final record"),
};
let record = match agent {
Agent::Codex => {
transcript.apply_codex_metadata(&parsed);
ParsedRecord::from_codex_source(
index,
trimmed,
parsed,
&mut codex_tool_names,
&mut codex_spawn_prompts,
transcript.thread_source.as_deref(),
)
}
Agent::Claude => {
transcript.apply_claude_metadata(&parsed);
ParsedRecord::from_claude_source(index, trimmed, parsed, &mut claude_tool_names)
}View on GitHub (pinned to caf1f223d3)
Solutions
- Check whether the file is genuinely JSONL — one JSON object per line — and convert non-JSONL inputs before capture
- If two agents write the same file, give each its own session file and re-run capture; interleaved lines cannot be recovered automatically
- Recover by discarding or repairing the specific malformed lines (jq -c per line, or a repair script), then re-run the hook
- If the corruption is a single truncation point, split at the last valid line and capture the valid prefix
Example fix
// before
Err(_) => bail!("transcript contains malformed JSON before the final record"),
// after: skip corrupt non-final lines with a warning instead of aborting
Err(_) => {
warn!("skipping malformed transcript record at index {index}");
continue;
} Defensive patterns
Strategy: validation
Validate before calling
// Validate the snapshot is JSONL before capture
for (i, line) in snapshot.split(|b| *b == b'\n').enumerate() {
let last = /* caller knows if this is the final line */;
if !last && serde_json::from_slice::<serde_json::Value>(line.trim_ascii_end()).is_err() {
eprintln!("snapshot has malformed JSON at line {i}; repair before capture");
}
} Type guard
fn jsonl_line_is_valid(line: &[u8]) -> bool {
line.iter().all(|b| b.is_ascii_whitespace())
|| serde_json::from_slice::<serde_json::Value>(line).is_ok()
} Try / catch
match TranscriptBatch::parse(agent, &snapshot) {
Ok(batch) => batch,
Err(err) if err.to_string().contains("malformed JSON before the final record") => {
// repair or split the file at the last valid line and retry
retry_with_valid_prefix(snapshot)
}
Err(err) => return Err(err),
} Prevention
- One agent process per transcript file; never let two writers append concurrently
- Only append whole lines to JSONL transcripts (flush per line)
- Validate third-party transcript files with a per-line JSON check before feeding them to the hook
- Capture atomically: copy the snapshot and parse the copy so in-progress tail writes do not matter
When it happens
Trigger: Feeding `but agentlog hook` (capture) a transcript snapshot where a non-final line fails `serde_json::from_slice` — e.g. two processes interleaving appends to the same session file, a rotated/truncated file, or a non-JSONL file passed as the snapshot.
Common situations: Parallel agent runs sharing one session/transcript file; a log-rotation or backup tool truncating or interleaving the file mid-write; capturing a file that is not NDJSON (e.g. a JSON array export); disk corruption.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- existing GitMeta key '{associated_targets_key}' is not a str
- existing GitMeta key '{detail_key}' is not a string
- existing GitMeta key '{turns_key}' is not a list
- Invalid conflict stage '{}'
- Failed to parse parameters: {e}
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/c9481332d5b4bac4.
Report an issue: GitHub.