Redaction and retention¶
Session logs contain real prompts and possibly sensitive data. Hansard treats redaction and retention as design decisions, not afterthoughts — both are on by default, and both are deliberately conservative.
Redaction: on by default, high-confidence secrets only¶
Every session is scrubbed for high-confidence secret shapes before any of it touches disk:
- API keys (
sk-…,ghp_…,github_pat_…,AKIA…/ASIA…) Authorization: Bearer …tokens- PEM private key blocks (RSA/EC/DSA/OpenSSH/PKCS#8)
password=/api_key=/secret=-style assignments — and their structured-value counterpart, a dict key namedpassword,apikey,secret, etc.
Applied to every place caller data enters the log: message text,
agent.action.args, agent.action_result.output, turn.output() text,
session.note() text, session.correct(value=...), and the caller-supplied
part of session(meta=...) (the SDK's own agent/app/host/pid/
hansard_version fields are never candidates). Recursed through nested
dicts/lists/tuples up to a depth of 20, to protect the never-block-the-caller
guarantee against a pathological, self-nested structure.
Token shape, not just length¶
A shape-only regex (right length, right character class) also matches
ordinary English -- "Bearer authentication" and "secret: nobody" pass the
same length/charset test a real bearer token or password does. Raising the
length threshold doesn't fix this: "authentication" is 14 characters, longer
than plenty of real short tokens. The property that actually distinguishes a
credential from a word is that credentials are almost always alphanumeric
with at least one digit, and ordinary English words almost never are. So the
bearer_token/password/api_key/secret patterns additionally require
the matched value to contain a digit before treating it as a match. This is
one cheap, well-justified heuristic — not a perfect one; an all-alphabetic
secret (unusual in practice) is missed, which is the direction this module
is built to fail in. See hansard.redaction's benign-prose corpus
(tests/test_redaction.py) for the sentences this is checked against.
Every redaction records what kind of thing was removed, in the
existing redacted field (["api_key"]) — the transcript shows that
something was scrubbed without showing what. user_id is never redacted:
identity is the one thing the whole product depends on being exact.
Deliberately not redacted by default¶
Emails, names, URLs, and IP addresses. Those are frequently the actual
content of a multiplayer agent session — a transcript full of
[REDACTED] is worth much less than one that reads. Available as opt-in
categories:
hansard.Redactor(optional={"email", "url", "ip_address"})
Personal names are not offered as a category at all, even opt-in: unlike the shapes above, a name has no reliable regex signature — it is indistinguishable from a project name, a place, or a tool name without an NER model, which this project's zero-runtime-dependency, stdlib-only constraint rules out. A pattern that mostly misses real names while flagging ordinary nouns would be worse than not offering one at all — see Design decisions on why over-matching is treated as strictly worse than under-matching here.
The irreversibility problem¶
Redaction runs before anything touches disk, which is the point — but it means a bug in the redactor destroys evidence permanently, with no way to recover it. Every pattern in the default set is written to bias toward missing an unusual-looking secret rather than eating an ordinary one: under-matching is recoverable in spirit (nothing here claims to be a complete secret scanner), over-matching is not recoverable at all.
To disable redaction entirely — for callers with their own upstream scrubbing, or who genuinely need verbatim capture:
hansard.session(path="./sessions", agent="my-agent", redact=None)
prune: dry-run by default¶
hansard prune <dir> --older-than 30d [--yes]
Deleting audit logs is the most destructive thing this tool can do, and
the failure mode of an accidental prune is unrecoverable evidence loss.
So:
- Dry-run by default.
pruneprints what it would delete without touching anything.--yesis required to actually delete. Exit code is0if every session in the target directory could be classified,1if any session was unreadable (see below) and therefore left out of the classification — dry-run or not, an unreadable session is something a human should be told about, not silently absent from the report. - Age comes from the session's own recorded timestamps — the last event's, specifically — never filesystem mtime. A restored backup has fresh mtimes and stale contents; using mtime would silently keep the wrong sessions. Using the session's first event instead of its last would be its own bug: a session that started three months ago but received a correction yesterday is not stale evidence, it's one someone is still actively working with.
- Fails closed on unreadable input. A directory prune cannot
positively identify as a Hansard session (no
.jsonlsegments, or ones that don't parse) is never a deletion candidate. A directory prune can identify but cannot fully read — corrupt, truncated, no parseable events to derive an age from — is reported as unreadable and also left alone: that unreadable state may be exactly the evidence someone needs to diagnose why it's corrupt, and pruning it would destroy the one copy.
See CLI reference for real captured prune output, both the
"nothing eligible" and "would delete" cases.