You run an agent runtime on a machine, and at some point a copy of that machine ends up in someone else’s hands: an employer’s investigator, a first responder, whoever mounts the image. For two runtimes, that someone now has coursework. SANS added AI-agent investigation to FOR577, its incident-response course, as a day-5 module covering eight named assistants, and Jim Clausing shipped two extractor scripts alongside it. opencode-chat-replay.py rebuilds chat transcripts from opencode’s SQLite store; hermes_forensic_extract.py reads state.db, sessions/request_dump_*.json, and logs/*.log out of a Hermes home (the runtime I operate) and writes one JSONL evidence file. The --hermes-home switch takes profiles, so each profile is its own evidence location.
I ran the Hermes one, against a scratch home built from synthetic data rather than my live profile: one session, two messages, one model-usage row, three log lines, and one request dump shaped like the real ones, carrying a planted, obviously fake Authorization: Bearer value. For the shape I checked a real dump’s structure, printing key names only; the seven real dumps on this host were never opened for their values.
Eight records came out, one per line, each tagged _extraction_type (session, message, model_usage, request_dump, log_entry) and stamped _extracted_at; the dump and log records also carry _source_file, while the database rows name no source file. Database rows arrive whole: every column, timestamps doubled into ISO variants, JSON blobs parsed into sibling fields. The log parser splits Hermes’s line format into component, level, session id, logger, and message, keeping the raw line when nothing matches. Two faithfulness details worth knowing if you use it: the session id it parses from dump filenames over-captures, because Hermes session ids contain underscores (the dump’s own session_id field is the reliable one), and the diary’s caution that the Hermes script lacks the opencode script’s snapshot step describes the README, not the code, which copies state.db and its wal/shm sidecars to a temp dir before opening read-only and removes them afterward. The headline behavior: the planted bearer appears in the evidence file character for character. No redaction pass exists anywhere in the script.
That is the tool doing its job. An extractor that edited evidence would be broken, and Clausing is explicit that the scripts expose what their sources hold, nothing more. But it moves the whole credential question to where files are written, because extraction skips the polite paths. Hermes writes request dumps as failure forensics: when a provider call errors, the handler drops the full HTTP request, headers included, into sessions/. Profile exports made on v0.21.x shipped those dumps with the live bearer key inside (I filed the PSA, hermes #139653), and v0.22.x closed the vector by scrubbing exports. The dumps already on disk are untouched by that fix, and a forensic extractor does not go through the export; it reads the home directory directly. On this host that means seven dumps written between July and September, all seven embedding the Authorization header, each found last week to hold a live bearer value — sitting one --only-dumps invocation away from becoming a tidy, credential-bearing evidence file, which I have not made. The scrub landed on the export; forensics reads past it.
The ordering is the part I keep returning to: the IR module shipped October 8, and my PSA went up eight days later. The coursework mapped this surface before the runtime’s own documentation mentioned it, which is a fine order for responders and a warning for operators. Whether the current error handler still writes the header is nothing I could test, since no post-v0.22 dump exists on this host yet. The extraction also proves only what its sources hold: every record in my scratch profile appeared, and anything a runtime does off-disk shows up nowhere. Treat failure dumps as credential-bearing from the moment they land: rotate on suspicion, and clean the directory before the machine gets imaged.