Load a transcript to begin
Drop one or more session files. Format is detected per file, so you can mix agents in one review. Pasted screenshots render inline.
Drop one or more session files. Format is detected per file, so you can mix agents in one review. Pasted screenshots render inline.
Each rule is an object with id, label, severity
(high, med or info) and a JavaScript regular expression
pattern, optionally with flags and a kinds array limiting it to
certain entry types (message, thinking, tool_call,
tool_result, edit, context, meta).
Patterns are matched against the entry text, tool name, tool input and tool output.
Applying re-scores every loaded session.
| Sev | Flag | Agent | Session | Time (UTC) | Tool | Excerpt |
|---|
All eleven supported assistants write their conversations to disk in plain text or SQLite, with no server round trip needed to read them. Transcripts are per-user and per-machine, so collection is a filesystem sweep rather than an API pull. Append-only formats mean a file grabbed mid-session is a valid partial transcript, not a corrupt one.
| Assistant | Storage | Lives in |
|---|---|---|
| Aider | Markdown | the repository |
| Claude Code | JSONL per session | user profile |
| Cline | JSON per task | VS Code extension storage |
| Codex CLI | JSONL per session | user profile |
| Cursor | SQLite key-value | app support directory |
| DeepSeek Harness | JSONL event stream (zstd), or SQLite | configurable, storage is a plugin |
| Gemini CLI | JSONL per session | user profile |
| Copilot Chat | JSON or JSONL | VS Code storage, or the repo in Visual Studio |
| Goose | JSONL per session | user profile |
| Hermes Agent | SQLite, one shared database | user profile |
| OpenCode | SQLite, one shared database | user profile |
.vs\ in the solution directory. A sweep of home
directories misses both, and because the files sit in the repository they can be committed and
pushed. Include a pass over your source trees, and check whether these paths are gitignored.
Four of the ten keep everything in SQLite, and every one of those uses write-ahead logging. In each case the -wal sidecar must be collected with the database or you will silently lose the most recent sessions. That single mistake accounts for more empty collections than any other.
Expect a few KB for a short session up to tens of MB for a long one containing pasted screenshots, since images are stored inline as base64. Budget accordingly on constrained EDR upload paths.
~/.local/share/opencode/auth.json, ~/.claude/.credentials.json, and any config file holding API keys. They add no investigative value and expand your exposure.
Hash on acquisition and record the source host and user, since none of these formats carry an internal identity claim you can trust. Nothing in a transcript is signed, and the files are user-writable, so they are evidence of what the tool recorded rather than proof of what a person did. This viewer never uploads anything, which means you can safely open evidence copies on an analysis workstation without an approval conversation.
ls the profile paths, then get each transcript. Batch across hosts with a custom RTR script for the glob list.findfile rollout-*.jsonl, then getfile. Note the file-size ceiling on live response transfers when sessions contain images..tkape target using the glob list collects all four in one pass.tar czf sessions.tgz ~/.codex/sessions ~/.claude/projects ~/.local/share/opencode/opencode.db*These cover all ten assistants. Adjust the drive letter and profile root for your environment. The trailing * on database paths is deliberate: it pulls the -wal and -shm sidecars.
Windows - user profile C:\Users\*\.codex\sessions\**\rollout-*.jsonl C:\Users\*\.codex\history.jsonl C:\Users\*\.claude\projects\**\*.jsonl C:\Users\*\.claude.json C:\Users\*\.claude\settings.json C:\Users\*\.gemini\tmp\*\chats\*.jsonl C:\Users\*\.gemini\tmp\*\logs.json C:\Users\*\.hermes\state.db* C:\Users\*\.local\share\opencode\opencode*.db* C:\Users\*\.local\share\goose\sessions\*.jsonl C:\Users\*\AppData\Roaming\goose\**\sessions\*.jsonl C:\Users\*\AppData\Roaming\Cursor\User\globalStorage\state.vscdb* C:\Users\*\AppData\Roaming\Cursor\User\workspaceStorage\*\state.vscdb* C:\Users\*\AppData\Roaming\Code\User\globalStorage\*claude-dev*\tasks\**\*.json C:\Users\*\AppData\Roaming\Code\User\globalStorage\*roo*\tasks\**\*.json C:\Users\*\AppData\Roaming\Code\User\workspaceStorage\*\chatSessions\* C:\Users\*\AppData\Roaming\Code - Insiders\User\workspaceStorage\*\chatSessions\* macOS - user profile /Users/*/.codex/sessions/**/rollout-*.jsonl /Users/*/.claude/projects/**/*.jsonl /Users/*/.gemini/tmp/*/chats/*.jsonl /Users/*/.hermes/state.db* /Users/*/.local/share/opencode/opencode*.db* /Users/*/.local/share/goose/sessions/*.jsonl /Users/*/Library/Application Support/Cursor/User/globalStorage/state.vscdb* /Users/*/Library/Application Support/Cursor/User/workspaceStorage/*/state.vscdb* /Users/*/Library/Application Support/Code/User/globalStorage/*claude-dev*/tasks/**/*.json /Users/*/Library/Application Support/Code/User/workspaceStorage/*/chatSessions/* Linux - user profile /home/*/.codex/sessions/**/rollout-*.jsonl /home/*/.claude/projects/**/*.jsonl /home/*/.gemini/tmp/*/chats/*.jsonl /home/*/.hermes/state.db* /home/*/.local/share/opencode/opencode*.db* /home/*/.local/share/goose/sessions/*.jsonl /home/*/.config/Cursor/User/globalStorage/state.vscdb* /home/*/.config/Code/User/globalStorage/*claude-dev*/tasks/**/*.json /home/*/.config/Code/User/workspaceStorage/*/chatSessions/* Repository trees - sweep your source checkouts, not the profile **/.aider.chat.history.md **/.aider.input.history **/.aider.llm.history **/.vs/*/copilot-chat/**/sessions/* Do not collect - credentials, no investigative value **/.hermes/config.yaml (only if it holds keys) **/opencode/auth.json **/.claude/.credentials.json
Work in two phases: sweep the fleet for presence and last-activity without moving any data, then collect only from the hosts that matter. Driving both phases from one table of globs keeps the two in step and gives you agent attribution for free.
Phase 1 — triage sweep. No file transfer, so it is cheap enough to run estate-wide.
LET targets = SELECT * FROM parse_csv(accessor="data", filename='''
agent,glob
aider,C:/Users/*/**/.aider.chat.history.md
aider,C:/Users/*/**/.aider.input.history
claude_code,C:/Users/*/.claude/projects/**/*.jsonl
cline,C:/Users/*/AppData/Roaming/Code/User/globalStorage/*claude-dev*/tasks/**/*.json
codex,C:/Users/*/.codex/sessions/**/rollout-*.jsonl
codex,C:/Users/*/.codex/history.jsonl
copilot,C:/Users/*/AppData/Roaming/Code/User/workspaceStorage/*/chatSessions/*
copilot,C:/Users/*/AppData/Roaming/Code - Insiders/User/workspaceStorage/*/chatSessions/*
cursor,C:/Users/*/AppData/Roaming/Cursor/User/globalStorage/state.vscdb*
cursor,C:/Users/*/AppData/Roaming/Cursor/User/workspaceStorage/*/state.vscdb*
dsh,C:/Users/*/.dsh/**/*.jsonl*
dsh,C:/Users/*/.dsh/**/*.db*
gemini,C:/Users/*/.gemini/tmp/*/chats/*.jsonl
gemini,C:/Users/*/.gemini/tmp/*/logs.json
goose,C:/Users/*/.local/share/goose/sessions/*.jsonl
goose,C:/Users/*/AppData/Roaming/goose/**/sessions/*.jsonl
hermes,C:/Users/*/.hermes/state.db*
opencode,C:/Users/*/.local/share/opencode/opencode*.db*
''')
SELECT agent, OSPath, Size, Mtime, Btime
FROM foreach(row=targets, query={
SELECT agent, OSPath, Size, Mtime, Btime FROM glob(globs=glob)
})
WHERE Mtime > now() - 86400 * 90
ORDER BY Mtime DESC
Because transcripts are append-only, Mtime is a sound proxy for last agent activity and Btime for session start. That alone answers "which developers are running which assistants, and when did they last use them" before you read a single conversation.
Phase 2 — collection. Same table, with the files pulled back:
SELECT agent, OSPath, Size, Mtime,
upload(file=OSPath) AS Upload
FROM foreach(row=targets, query={
SELECT agent, OSPath, Size, Mtime FROM glob(globs=glob)
})
WHERE Mtime > now() - 86400 * 30
glob(globs=glob, accessor="ntfs") and upload(file=OSPath, accessor="ntfs") to read past the lock via the raw filesystem. On macOS and Linux there is no equivalent accessor, so either stop the agent or shell out to sqlite3 <db> ".backup out.db" and collect the copy.
db* are deliberate. They pull -wal and -shm alongside the database. Narrowing them to .db will hand you a database missing its most recent sessions.
Packaging. Wrap it as a custom artifact so it is repeatable and parameterised:
name: Custom.Forensics.AIAssistantSessions
description: |
Locate and collect coding-assistant transcripts (Claude Code, Codex,
Gemini CLI, Hermes, OpenCode, Copilot Chat) from user profiles.
parameters:
- name: MaxAgeDays
default: "30"
- name: Accessor
default: "auto"
description: Set to "ntfs" to read databases locked by a running agent.
- name: Upload
type: bool
default: "Y"
sources:
- query: |
LET targets = SELECT * FROM parse_csv(accessor="data", filename=GlobTable)
SELECT agent, OSPath, Size, Mtime, Btime,
if(condition=Upload, then=upload(file=OSPath, accessor=Accessor)) AS Upload
FROM foreach(row=targets, query={
SELECT agent, OSPath, Size, Mtime, Btime
FROM glob(globs=glob, accessor=Accessor)
})
WHERE Mtime > now() - 86400 * atoi(string=MaxAgeDays)
The aider rows assume repositories sit under the user profile. If developers check out to a separate volume, add a second table scoped to those roots — Aider transcripts are the one source a profile-only sweep will always miss.
If you would rather not write VQL, the built-in Generic.Collectors.File artifact accepts the glob list directly, and on Windows a custom Windows.KapeFiles.Targets target does the same job inside an existing KAPE workflow.
Do not widen the globs to whole directories. ~/.local/share/opencode/ and ~/.hermes/ both contain auth.json and credential files that add nothing to an investigation and expand what you are now holding. The patterns above are scoped to transcripts and databases on purpose.
Note for older deployments: Velociraptor renamed the glob output column from FullPath to OSPath. If your server predates that change, substitute the older name throughout.
| What | Path |
|---|---|
| Chat history | <repo>/.aider.chat.history.md — full conversation as markdown |
| Prompt history | <repo>/.aider.input.history — every prompt typed, with timestamps |
| Raw LLM traffic | <repo>/.aider.llm.history — only present when --llm-history-file was used |
| Config | ~/.aider.conf.yml, <repo>/.aider.conf.yml, .aider.model.settings.yml |
Aider is the only supported assistant whose transcript is plain markdown rather than structured records, so there are no tool-call objects to parse. This viewer reconstructs turns from the file's own conventions: #### prefixes user prompts, unprefixed text is the model, and > lines are command output and applied-edit notices. File changes appear as search-and-replace blocks inside model responses rather than as tool calls, so they will not surface under the File edit filter.
Check .gitignore during review. If these files are not ignored, they may already be in the repository history, and the prompt history in particular routinely contains pasted credentials.
| Platform | Path |
|---|---|
| macOS / Linux | ~/.claude/projects/<escaped-working-dir>/<session-uuid>.jsonl |
| Windows | %USERPROFILE%\.claude\projects\<escaped-working-dir>\*.jsonl |
| Also useful | ~/.claude.json and ~/.claude/settings.json (permissions, allowed tools, MCP servers), ~/.claude/shell-snapshots/, ~/.claude/todos/ |
| Override | $CLAUDE_CONFIG_DIR |
The directory name is the project's working directory with path separators replaced, so the folder tree itself tells you which repositories were worked on before you open a single file. Settings files are worth collecting alongside transcripts: they establish which tools were pre-approved, which is the difference between "the agent ran this" and "someone let it".
Cline is a VS Code extension, so its data sits in the editor's extension storage rather than a dotfile in the home directory. Each task gets its own directory named with an epoch-millisecond id, which gives you a usable timeline before you open anything.
| Platform | Path |
|---|---|
| Windows | %APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\tasks\<taskId>\ |
| macOS | ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/<taskId>/ |
| Linux | ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/<taskId>/ |
| Files | api_conversation_history.json is the one to collect. ui_messages.json holds the rendered view and adds little. |
api_conversation_history.json is a plain array of provider-shaped messages, so it is exactly what was sent to the model, including tool calls and results. Drop that file directly.
The forks share this layout under different publisher ids — Roo Code as rooveterinaryinc.roo-cline and Kilo Code as kilocode.kilo-code. Widen the glob to globalStorage/*claude-dev* and globalStorage/*roo* to catch them, since the file format is the same.
| Platform | Path |
|---|---|
| macOS / Linux | ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl |
| Windows | %USERPROFILE%\.codex\sessions\YYYY\MM\DD\rollout-*.jsonl |
| Also useful | ~/.codex/history.jsonl (prompt history), ~/.codex/config.toml (approval and sandbox policy), ~/.codex/log/ |
| Override | $CODEX_HOME relocates the whole tree |
The date-partitioned directories make time-scoped collection easy: pull only the days covering your incident window instead of the whole tree.
Cursor is a VS Code fork and inherits its storage design: a SQLite key-value store rather than per-session files. Conversations live as JSON documents inside table rows.
| Platform | Path |
|---|---|
| Windows | %APPDATA%\Cursor\User\globalStorage\state.vscdb |
| macOS | ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb |
| Linux | ~/.config/Cursor/User/globalStorage/state.vscdb |
| Per workspace | .../User/workspaceStorage/<hash>/state.vscdb — collect these too, they hold workspace-scoped chats |
| Sidecars | state.vscdb-wal and state.vscdb-shm |
Two places hold conversation data. The cursorDiskKV table stores composer sessions under composerData:<uuid> keys, and ItemTable holds aiService.prompts, a flat list of every prompt typed. This viewer reads both and presents the prompt history as its own session, which is often the fastest way to see what someone was asking for across a long period.
state.vscdb alone can be missing the most recent conversations. Cursor also holds the database open while running, so use the ntfs accessor on Windows or close the editor first.
Harness is DeepSeek's own agent runtime, released under MIT in August 2026 as a developer preview. Its design principle is that everything is a plugin — including session storage — and its stated invariant is that anything the model can see must be reconstructable from the log. For a reviewer that makes it the most complete transcript of any assistant here: system prompts, reasoning, tool calls and results, subagent scheduling and every context injection are separate event types rather than being folded into message text.
| What | Where |
|---|---|
| Session log | append-only JSONL, one SessionEvent per record: type, seq, time, data |
| Alternate backend | SQLite, where each event is a row of (session_id, seq, type, time, data). This viewer reads both. |
| Profile | the profile directory holds package.json with the bundle list and cordis.patch.yml with configuration |
| Config | home-level and command-line patches modify the final composition, so the effective config is not one file |
cordis.patch.yml and any home-level patch to find where sessionPersistence actually writes, then build your glob from that. A profile-based sweep that assumes a default directory will quietly return nothing on a host that has been reconfigured.
zstd -d session.jsonl.zst. This viewer detects the zstd magic bytes and tells you rather than failing oddly, but it cannot decompress in the browser — adding a decompressor would mean shipping a library, and an evidence tool should not fetch code at runtime.
Two details are worth attention during review. Sessions can be forked, and the fork point is recorded in the session header rather than the event stream, so a log that appears to start mid-task is a branch whose earlier turns live in the parent — this viewer surfaces the lineage as the first entry. And because a single JSONL record may pack several events, record counts are not turn counts; use the contiguous seq values to confirm you have an unbroken log rather than counting lines.
Note that several unrelated tools carry similar names, including a Rust terminal client (DeepSeek-TUI, config at ~/.deepseek/config.toml), a Node CLI and VS Code pair called Deep Code (~/.deepcode/settings.json), and a Go binary dsc with its own SQLite sessions. Confirm which one is actually installed before collecting; only Harness produces the event-stream format described above, though dsc databases will often open through the generic SQLite reader.
| What | Path |
|---|---|
| Session recordings | ~/.gemini/tmp/<projectHash>/chats/*.jsonl |
| Windows | %USERPROFILE%\.gemini\tmp\<projectHash>\chats\*.jsonl |
| Older builds | ~/.gemini/tmp/<projectHash>/logs.json — user prompts only, no model replies or tool calls |
| Manual saves | /chat save <tag> writes JSON into the same project temp directory |
| Checkpoints | a shadow git repo in the project history directory, committed as Gemini CLI <gemini-cli@google.com> |
sessionRetention setting and removes both session files and their tool-output artifacts once they exceed a maximum count or age. Unlike the other assistants, waiting to collect can mean the evidence is simply gone. If you are standing up collection for a fleet, Gemini is the one that needs a scheduled pull rather than on-demand acquisition.
The directory name is a SHA-256 of the project path, not the path itself, so it is opaque until you map it. Hash your candidate repository paths to match a hash back to a working directory.
Two artifacts pay off beyond the transcript. The shadow git repository holds per-turn snapshots of the workspace, which tells you exactly what changed during a given turn rather than what the agent said it changed. And rewind is recorded as a $rewindTo marker: on reload Gemini truncates history back to that message, but the discarded turns are still in the file. This viewer keeps them and tags them superseded, since a turn someone rewound past is often the interesting one.
| Platform | Path |
|---|---|
| Windows | %APPDATA%\Code\User\workspaceStorage\<workspace-id>\chatSessions\*.json |
| macOS | ~/Library/Application Support/Code/User/workspaceStorage/<workspace-id>/chatSessions/ |
| Linux | ~/.config/Code/User/workspaceStorage/<workspace-id>/chatSessions/ |
| Insiders | same paths with Code - Insiders in place of Code |
| Visual Studio | <solution-dir>\.vs\<solution-name>\copilot-chat\<hash>\sessions\ — note this sits in the repo, not the profile |
| User export | command palette, Chat: Export Session |
Copilot has changed storage shape across versions: recent builds write .jsonl where each line is a state object, older ones write a single .json per session. This viewer accepts both. The workspace ID is an opaque hash, so map it back to a repository via the workspace.json sitting beside the chatSessions folder.
| What | Path |
|---|---|
| Sessions | ~/.local/share/goose/sessions/*.jsonl |
| Config | ~/.config/goose/config.yaml — providers, extensions, and enabled MCP servers |
| Logs | ~/.local/state/goose/logs/ |
| Windows | layout varies by build; search the profile for goose\sessions\*.jsonl under %APPDATA% and %LOCALAPPDATA% rather than assuming a fixed path |
The first line of each session file is metadata carrying the working directory and a description; the remaining lines are messages whose content parts include toolRequest and toolResponse objects. Because Goose leans heavily on MCP extensions, review config.yaml alongside the transcript: the tools available in a session are defined there, and an unexpected extension is worth as much attention as an unexpected command.
Hermes keeps everything in one SQLite database shared by the CLI, the gateway, and any worktree agents. There are no per-session files; an earlier JSONL layout was replaced by this database.
| What | Path |
|---|---|
| Database | ~/.hermes/state.db plus state.db-wal and state.db-shm |
| Windows | %USERPROFILE%\.hermes\state.db* |
| Override | $HERMES_HOME relocates the whole directory |
| Also useful | ~/.hermes/config.yaml, ~/.hermes/session-exports/ |
state.db-wal at any moment. In testing, a freshly written Hermes database was 4 KB on disk with 111 KB of conversation still in the WAL: collecting state.db alone would have recovered nothing at all.
Two schema details matter for review. Sessions chain through parent_session_id when compression splits a long conversation, so one logical conversation can span several session rows — this viewer surfaces the link but you must open the parent to read the earlier turns. And messages.api_content records what was actually sent to the model when it differs from the stored content, which is where ephemeral memory and plugin injections show up; those are flagged as a wire-content mismatch.
The source column records whether a session came from the terminal or from Telegram, Discord, Slack, WhatsApp, or Signal. A coding agent driven from a chat app is flagged, since it changes who could have issued the instructions.
Alternative: hermes sessions export --format trace emits Claude Code JSONL, which this viewer already reads. Note that trace exports are secret-redacted by default; for a security review pass --no-redact, or you will be reviewing a transcript with the findings removed.
Storage changed. OpenCode 1.2 and later keep all chat history in a SQLite database. The old storage/ directory of JSON files is left behind as a shell containing only session_diff/ and a migration marker, so collecting that folder gets you nothing.
| What | Path |
|---|---|
| Database | ~/.local/share/opencode/opencode.db plus opencode.db-wal and opencode.db-shm |
| Windows | %USERPROFILE%\.local\share\opencode\opencode.db* |
| Legacy (pre-1.2) | ~/.local/share/opencode/project/<slug>/storage/ or .../global/storage/, holding session/, message/, part/ JSON |
| Other artifacts | storage/session_diff/, tool-output/, plans/, log/, repos/ |
| Overrides | $OPENCODE_DB sets the database path directly; $OPENCODE_DATA_DIR moves the whole tree |
| Channels | non-release builds write opencode-<channel>.db, so match opencode*.db rather than the exact name |
-wal file with the database. SQLite in WAL mode holds recent committed transactions in the write-ahead log until a checkpoint. Grabbing opencode.db on its own can silently miss the most recent session, which on a live-response collection is usually the session you care about. This viewer reads the WAL and overlays it, so drop both files together. The -shm file is a shared-memory index with no conversation data; it is harmless to collect and safe to skip.
Copying a database out from under a running process can catch a torn page. Where the tooling allows it, prefer sqlite3 opencode.db ".backup out.db", or stop OpenCode first. This viewer skips damaged cells rather than refusing the whole file, so a slightly torn copy still yields most of its sessions.