Skip to content

OALabs/asftriage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ASF Triage

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.

Live on OALABS

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.

Agent session logs

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/proj becomes -Users-me-proj). Stored in plaintext and kept 30 days by default (cleanupPeriodDays). The base directory moves with the CLAUDE_CONFIG_DIR environment 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.

Run

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

What it does

  • Multi-session workspace: logs are grouped by sessionId into 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-amber Agent boxes 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, labeled compaction (for agentId starting acompact-) or orphan (for everything else) so the analyst isn't fooled into thinking they were user-spawned. When the main <sessionId>.jsonl is 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>.jsonl paired with agent-<id>.meta.json. The meta always carries agentType; some captures also include description and toolUseId. Correlation prefers toolUseId (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).
  • 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.

Log format notes

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.

Codex CLI

asftriage also ingests Codex CLI logs:

  • Rollout sessions (*-rollout-*.jsonl): typed envelopes ({ timestamp, type, payload }). The whole file is one session, keyed by session_meta.payload.id (Codex records carry no per-line session id). User prompts come from event_msg.user_message (clean, with the injected AGENTS.md / environment / permissions context stripped), assistant text from response_item assistant messages, tools from function_call paired with function_call_output by call_id (plus web_search_call). Encrypted reasoning records 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 per session_id into a prompt-only timeline.

Kinds show as codex / codex-history badges in the Session Picker.

Architecture

  • 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 via src/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 in src/composables/.

Tests

npm test    # smoke test for the core parser/redactor/gap logic

The 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.

Greetz

Big thanks to NtLoadDriverEx for the original proof of concept and much help along the way.

About

LLM Agent Session Forensics Tool

Resources

Stars

Watchers

Forks

Contributors

Languages