Invariant
The hook is best-effort by design: every failure path exits 0 so session exit is
never noisy. "Best effort" must still mean it retries — a transient or
corrupt state must not become permanent.
Current violation (observed 2026-08-12)
A corrupt archive is never repaired.
existing="$(find "$archive_dir" -maxdepth 1 -name "*-${session_id}.jsonl.gz" | head -1)"
if [[ -n "$existing" && ! "$transcript" -nt "$existing" ]]; then
src_size="$(wc -c <"$transcript" | tr -d ' ')"
arch_size="$(gzip -l "$existing" | awk 'NR==2{print $2}')"
[[ "$src_size" != "$arch_size" ]] || exit 0
fi
If $existing is truncated or corrupted and is not older than the transcript,
gzip -l exits nonzero. The global trap 'exit 0' ERR then fires and the
script exits successfully, without rewriting the archive. Every later
SessionEnd for that session takes the same path, so the corrupt file survives
forever and the freshness check can never replace it.
Compounding it: exec 2>/dev/null means nothing is reported, and the archive
is the only copy once Claude Code's cleanupPeriodDays deletes the transcript.
Rated P2 rather than higher: writes are atomic (temp file + mv), so a corrupt
archive is unlikely to arise in the first place, and no new data is lost
beyond what the corruption already took.
Verify
tmp=$(mktemp -d); mkdir -p "$tmp/proj"
printf '{"a":1}\n' > "$tmp/proj/sess.jsonl"
printf 'not gzip' > "$tmp/archive/20260812-000000-proj-sess.jsonl.gz" 2>/dev/null \
|| { mkdir -p "$tmp/archive"; printf 'not gzip' > "$tmp/archive/20260812-000000-proj-sess.jsonl.gz"; }
touch "$tmp/archive/20260812-000000-proj-sess.jsonl.gz" # archive not older than transcript
CLAUDE_TRANSCRIPT_ARCHIVE_DIR="$tmp/archive" \
./templates/claude-hooks/session-end-archive/session-end-archive.sh <<JSON
{"session_id":"sess","transcript_path":"$tmp/proj/sess.jsonl","cwd":"$tmp/proj"}
JSON
gzip -t "$tmp/archive"/*.gz && echo REPAIRED || echo "STILL CORRUPT (violation)"
Prints STILL CORRUPT while violated; REPAIRED once fixed.
Acceptance criteria
Found while wiring this hook into harmon-init's devcontainer
(evanharmon1/harmon-init#816), where the script is vendored verbatim.
Invariant
The hook is best-effort by design: every failure path exits 0 so session exit is
never noisy. "Best effort" must still mean it retries — a transient or
corrupt state must not become permanent.
Current violation (observed 2026-08-12)
A corrupt archive is never repaired.
If
$existingis truncated or corrupted and is not older than the transcript,gzip -lexits nonzero. The globaltrap 'exit 0' ERRthen fires and thescript exits successfully, without rewriting the archive. Every later
SessionEnd for that session takes the same path, so the corrupt file survives
forever and the freshness check can never replace it.
Compounding it:
exec 2>/dev/nullmeans nothing is reported, and the archiveis the only copy once Claude Code's
cleanupPeriodDaysdeletes the transcript.Rated P2 rather than higher: writes are atomic (temp file +
mv), so a corruptarchive is unlikely to arise in the first place, and no new data is lost
beyond what the corruption already took.
Verify
Prints
STILL CORRUPTwhile violated;REPAIREDonce fixed.Acceptance criteria
gzip -lfailure does not reach theERRtrap as a success pathFound while wiring this hook into harmon-init's devcontainer
(evanharmon1/harmon-init#816), where the script is vendored verbatim.