Traces reference
Scope: where traces come from and what moose-tracevals reads out of them. Source of truth:
src/trace/, in detect.ts, claude.ts, and discover.ts.
Supported formats
Section titled “Supported formats”Format is sniffed from the content, not the filename. moose-tracevals scans lines until one identifies the format:
| Format | Identified by | Produced by |
|---|---|---|
claude-session |
parentUuid or sessionId on a record |
Claude Code session files, ~/.claude/projects/<slug>/*.jsonl |
claude-stream |
session_id on a record |
claude -p --output-format stream-json |
Both normalise into the same Trace model, so everything downstream is format-agnostic. The
TraceSource union is currently just claude-code; it is the seam for future adapters
(ADR 01003).
Lines that parse but identify nothing are skipped rather than treated as an error, so a leading
metadata record does not defeat detection. {"type":"summary"} is one such line.
Detection errors
Section titled “Detection errors”All exit 2:
| Message | Cause |
|---|---|
trace is not JSONL (supported formats: …) |
The file is not line-delimited JSON at all. |
unrecognized trace format (supported formats: …) |
Parseable JSONL, but no record carried a recognised key. |
empty or unparseable trace file (supported formats: …) |
Nothing in the file parsed. |
What is extracted
Section titled “What is extracted”| Field | Source |
|---|---|
skillInvocations |
Skill tool calls (input.skill, via: "skill-tool") and <command-name> injections in user text (via: "command-injection", leading / stripped, <command-args> captured). via is load-bearing: a command-injection entry names a slash command, which resolution later decides is a command file, a skill, or a built-in |
agentSpawns |
the Agent tool’s input.subagent_type, plus its description |
fileAccesses |
Read / Write / Edit input.file_path, mapped to op read / write / edit |
toolCalls |
every tool call, with a sidechain flag |
turnCount |
non-sidechain user prompts. The stream format overrides this with result.num_turns |
usage |
stream-json only, from result.total_cost_usd and token counts |
assistantTexts, userMessages |
message text, used by the regex and json-output graders |
availability |
the roster the session was offered: skills, agents, tools, and MCP servers. See The availability roster |
Unparseable lines are tolerated and counted, surfacing as a warning rather than an error:
Warnings ! 1 unparseable JSONL line(s) were skippedSession bookkeeping records become meta events and contribute nothing to the conversation:
queue-operation, attachment, last-prompt, custom-title, summary, mode, and pr-link.
Three kinds of attachment are the exception. They are read for the availability roster below.
The availability roster
Section titled “The availability roster”Everything above describes what the session used. The roster describes what it was offered, which is the only way to notice the skill that should have fired and didn’t.
Claude Code records it as structured attachment records, so it is recoverable exactly and
retroactively, so every session already on your disk has it:
attachment.type |
Becomes |
|---|---|
skill_listing |
availability.skills, with names and descriptions parsed from the listing text |
agent_listing_delta |
availability.agents, the subagent_types, with descriptions and tool grants |
deferred_tools_delta |
availability.tools, plus availability.mcpServers with a pending, needs-auth, or failed status |
Each entry is one stretch of availability:
{ "kind": "skill", "name": "writing-toolkit:technical-writer", "description": "Use when creating clear, accurate, user-focused documentation.", "offeredAt": 7, // ordinal in trace.events where it appeared "withdrawnAt": 412 // absent while still offered}A name withdrawn and later re-offered produces two entries rather than one with a hole, so
availableAt(entries, index) is a plain filter. Availability really is time-varying. A
directory-scoped skill appears mid-session when work moves into its tree. The exported helper
answers “what was on the menu at this point” rather than only “at some point”.
What the roster does not carry
Section titled “What the roster does not carry”- Descriptions are best-effort. The listing text is budget-truncated near 30,000 characters; past that, Claude Code lists skills by name alone. One real session offers 527 skills and describes 42 of them. An absent description means not recorded, never “has none”.
- Tools have no descriptions at all. The tool listing repeats each tool’s own name.
- Artifact bodies and their hashes are not in the trace. The roster settles what was available,
not what a
SKILL.mdsaid at the time.
Subagents have their own roster
Section titled “Subagents have their own roster”A sidecar transcript carries its own listing records, and its set is usually a subset of the parent’s. Only names the session never saw elsewhere are folded in, tagged with the branch that saw them. Replaying a sidecar’s listing as the session’s would withdraw the parent’s whole roster mid-session. Dropping it would report a subagent-only skill as never offered.
Discovery
Section titled “Discovery”moose-tracevals list and the interactive picker scan the session store:
<home>/.claude/projects/<project-slug>/*.jsonlResults are sorted by modification time, newest first, then truncated to --limit.
The project slug
Section titled “The project slug”A project directory maps to a store directory by replacing every non-alphanumeric character with a hyphen:
/work/demo-project → -work-demo-projectC:\work\demo-project → C--work-demo-projectThat is Claude Code’s own rule, reproduced so --project can find the right directory. The helper
is exported as slugFor().
Listing entries
Section titled “Listing entries”| Field | Notes |
|---|---|
file |
Absolute path. Pass this to run. |
mtimeMs |
Modification time; the sort key. |
sizeBytes |
File size. |
sessionId |
From the file, when present. |
project |
The recorded working directory. |
firstPrompt |
First non-sidechain user message that does not start with <command-, first line only, capped at 200 characters. |
To keep listing fast, only the first 256 KB of each file is read for the summary. A session
whose opening prompt sits past that window shows no firstPrompt; it is still fully parsed by run.
MOOSE_TRACEVALS_HOME
Section titled “MOOSE_TRACEVALS_HOME”Overrides the home directory for both trace discovery and user-level artifact lookup, meaning
~/.claude/skills/, ~/.claude/agents/, and the plugin store.
MOOSE_TRACEVALS_HOME=./session-store npx moose-tracevals list --all-projects- A relative value resolves against the current working directory.
- Unset, it falls back to the OS home directory.
Use it to make a run reproducible on a shared CI runner instead of inheriting whatever home the
runner has. This repository’s own CI sets MOOSE_TRACEVALS_HOME=test/fixtures/home for exactly that
reason.
Naming a trace directly
Section titled “Naming a trace directly”run takes any path; it does not have to live in the session store.
npx moose-tracevals run ./traces/refactor-session.jsonl --project ./my-repoThis is the pattern for committing a small corpus of representative sessions and re-evaluating them
whenever instructions change. Pair it with --project, because the recorded cwd will not exist on
the machine running the check.