A forensic investigation tool for AI agent session logs (Claude Code and Codex CLI).
Drop one or more session .jsonl transcripts, a history.jsonl, a whole folder, or a
saved case file, and get a chat-like view for investigating insider-threat and malicious
sessions.
Runs client-side in the browser; evidence never leaves the analyst's machine. The one exception is per-box translation, which is opt-in and warns before sending text to Google.
If you don't want to run a local copy we host a live version that you can use directly from your browser: ASF Triage
Even with the hosted version evidence never leaves the analyst's machine! Everything is processed locally in your browser.
AI coding agents keep a local, append-only JSONL transcript of every session: user prompts, model responses, internal reasoning, and each tool call with its output. It is written automatically so a session can be resumed. For an investigation these files are primary evidence; they reconstruct what an operator asked the agent to do and what it ran on the host. asftriage reads them directly.
The paths below are where the logs live by default, per the Claude Code and Codex CLI
docs. They are home-relative, so ~ resolves per OS:
- macOS:
/Users/<user> - Linux:
/home/<user> - Windows:
%USERPROFILE%(e.g.C:\Users\<user>, with\separators)
So Claude Code transcripts sit under /Users/<user>/.claude/projects/... on macOS and
%USERPROFILE%\.claude\projects\... on Windows.
Claude Code
- Session transcripts:
~/.claude/projects/<encoded-cwd>/<session-id>.jsonl, where<encoded-cwd>is the session's working directory with every non-alphanumeric character replaced by-(so/Users/me/projbecomes-Users-me-proj). Stored in plaintext and kept 30 days by default (cleanupPeriodDays). The base directory moves with theCLAUDE_CONFIG_DIRenvironment variable. - Prompt history:
~/.claude/history.jsonl.
Codex CLI
- Rollout sessions (one file per session):
~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl. - Command history:
~/.codex/history.jsonl(disable with[history] persistence = "none"). - The base directory is
$CODEX_HOME, which defaults to~/.codex.
A single sessionId can span many days via resumes, so collect the whole projects/
(Claude Code) or sessions/ (Codex) tree, not just the most recent file.
npm install
npm run dev # http://localhost:5173
npm run build # production bundle in dist/
npm run preview # serve the built dist/ locally to check it- Multi-session workspace: logs are grouped by
sessionIdinto a Session Picker (preview cards: model, tool counts, time span, first prompt). Open sessions as tabs. - Global activity timeline: every loaded session as a swimlane on a shared time axis with activity density and resume gaps; brush to zoom, click a lane to jump in.
- Supertimeline: from the Sessions tab, stitch every session into one scrollable view (start-time order) with a full page-break header carrying each session's info between them, so all sessions can be reviewed in a single pass. Opens as a pinned tab; the open state and active view are saved in the case file.
- Chat-like timeline: user prompts (offset left), assistant responses, internal monologue (thinking), and tool calls + results (paired), each individually collapsible; collapse-all per type from the toolbar.
- Sub-agent threads:
Task/Agent(Claude Code ≥ 2.1) calls render as their own first-class event type — warm-amberAgentboxes left-justified like a user prompt (an Agent call is "the start of a sub-agent loop"), with their own Collapse pill in the toolbar and their own type-scope in the search bar. Below each Agent card sits an Expand button that splices the sub-agent's events into the timeline inline, each child bordered by a continuous warm-amber bar so the analyst sees the thread's extent at a glance. When the sub-agent transcript wasn't loaded, the slot under the card shows a red "No sub-agent transcript loaded" pill instead. Sub-agent transcripts that have no parent Agent call in the loaded data (e.g. the analyst dropped only sidecars, or Claude Code's auto-compaction snapshots which have no spawning call) are still surfaced — as synthetic Agent boxes placed chronologically by the first child's timestamp, labeledcompaction(foragentIdstartingacompact-) ororphan(for everything else) so the analyst isn't fooled into thinking they were user-spawned. When the main<sessionId>.jsonlis missing entirely, the session opens with a persistent red banner explaining the gap. Forensic-tool principle: never silently drop data. Two on-disk layouts are supported:- Legacy (≤ ~2.0): sidechain records inlined in the main transcript with
isSidechain: true, correlated by parent-Task prompt match. - v2.x sidecar: sub-agents live in
<session-id>/subagents/agent-<id>.jsonlpaired withagent-<id>.meta.json. The meta always carriesagentType; some captures also includedescriptionandtoolUseId. Correlation preferstoolUseId(deterministic) when present, otherwise falls back to root-prompt match. Drop the project tree or the session subdir so the sidecars come along. Sub-agent events are first-class in the timeline (search and flag-nav reach them; jumping auto-expands the parent thread).
- Legacy (≤ ~2.0): sidechain records inlined in the main transcript with
- VSCode-style minimap: a colored content-pattern overview on the right for fast navigation of very large sessions (tested on a 212 MB / 78 k-record transcript).
- Redaction: non-destructive, reversible masking of IPs, hostnames, emails, URLs, usernames, credit cards (Luhn-checked), keys/tokens/PEM. Toggle restores the exact original; search still matches the underlying text.
- Flags / tags: colored flags with notes on any box (also a colored separator line + minimap tick) and a cross-session flag navigator; add, edit, and delete from the flag button on each box.
- Timezone: timestamps default to UTC; convert to any IANA timezone from the toolbar.
- Activity gaps: a thick divider marks intra-session resume/idle gaps (default >= 60 min, live-adjustable).
- Context-aware search: by text, scoped to a type and to the current / open / all sessions, with match navigation that jumps to the right tab.
- Translate: per-box translation to English (cached, reversible). Sends text to Google; opt-in and warned.
- Case files: save the whole workspace (sessions, flags, settings, translations) to a
self-contained
.json(gzip for very large cases) and reload it later.
Validated against real transcripts. Records are line-delimited JSON; a user record is a
real prompt only when its content is a string/text block (otherwise it carries a
tool_result). Assistant content blocks are text / thinking / tool_use. A single
sessionId can span weeks via resumes, so session boundaries are by sessionId, not time.
Claude Code v2.x adds several top-level record types beyond user / assistant / system.
asftriage surfaces every one as a system timeline event (with a stable subtype) so the
analyst can see session-state transitions instead of having them silently dropped:
mode, permission-mode, agent-setting, last-prompt, queue-operation,
attachment, file-history-snapshot. Unrecognized record types fall through a catch-all
that emits a system event with the raw payload as text — nothing is ever dropped silently.
asftriage also ingests Codex CLI logs:
- Rollout sessions (
*-rollout-*.jsonl): typed envelopes ({ timestamp, type, payload }). The whole file is one session, keyed bysession_meta.payload.id(Codex records carry no per-line session id). User prompts come fromevent_msg.user_message(clean, with the injected AGENTS.md / environment / permissions context stripped), assistant text fromresponse_itemassistant messages, tools fromfunction_callpaired withfunction_call_outputbycall_id(plusweb_search_call). Encryptedreasoningrecords only yield a thinking event when plaintext summary/content is present. Metadata extracted: model, provider, originator, CLI version, cwd, and the system prompt (base_instructions). - Command history (
.codex-history.jsonl):{ session_id, ts(epoch-seconds), text }, grouped persession_idinto a prompt-only timeline.
Kinds show as codex / codex-history badges in the Session Picker.
- Vue 3 + Pinia + Tailwind v4, built with Vite.
- Parsing runs in a Web Worker (
src/workers/parser.worker.js) that streams files and normalizes them viasrc/lib/normalize.js. - The timeline is virtualized with
@tanstack/vue-virtual; the minimap and activity timeline are hand-rolled<canvas>. - Core logic is framework-free in
src/lib/(normalize,redactors,gaps,translate,caseFile); shared Vue logic lives insrc/composables/.
npm test # smoke test for the core parser/redactor/gap logicThe smoke test (tests/smoke.mjs) runs the framework-free src/lib/ code against small
inline fixtures, so it needs no sample data and no browser.
Big thanks to NtLoadDriverEx for the original proof of concept and much help along the way.