trace

package
v1.121.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

Chainloop Trace: agent support

Chainloop Trace records the sessions of AI coding agents as CHAINLOOP_AI_CODING_SESSION evidence. Each agent is a provider in this package: claude/, cursor/ and opencode/. The interfaces are in provider.go, and the registry is in providers/. One provider covers OpenCode 1.x and OpenCode 2: it detects the version at runtime.

This page lists the features of Trace, by area, and tells which agents support each one. Keep it current. When you add or change a provider, or add or change a feature of Trace, update this page in the same pull request.

In the tables, Yes means supported, Partial means supported with limits, No means not supported, and Unknown means not verified. Below each table, "Why not Yes" tells the reason for each cell that is not Yes.

Claude Code has the most complete support.

Session collection

What the session evidence holds about the conversation.

Feature What it means Claude Code Cursor OpenCode 1.x OpenCode 2
Transcript The evidence holds the conversation (raw_session): the messages and the tool calls of the agent. Yes Yes Partial Partial
Token usage and cost The evidence holds the tokens that the session used and their cost in USD. Yes No Yes Yes
Model detection The evidence names the primary model and the other models that the session used. Yes Partial Yes Yes
Subagents The evidence holds the conversations of the subagents, and their token usage, as part of the session. Yes No No No

Why not Yes:

  • Transcript, OpenCode: OpenCode has no transcript file. The provider runs opencode export (1.x) or opencode session export (2) and rebuilds the transcript from the export. It keeps the text and the inputs of the tool calls. It drops the tool outputs, the reasoning and the file parts. OpenCode 2 exports also have no agent version and no session slug.
  • Token usage and cost, Cursor: the Cursor transcript does not hold token usage. Usage and cost are zero, and the evidence has a warning that tells so. (For OpenCode, the cost is the value that OpenCode reports. For Claude Code, Chainloop computes it from its price table.)
  • Model detection, Cursor: the primary model comes from the session-start hook. The transcript does not tell the models, so the evidence does not list the other models.
  • Subagents, Cursor: the provider does not read subagent conversations.
  • Subagents, OpenCode: a subagent runs in a child session with its own export. It is not part of the evidence of its parent session.

Code attribution

Which lines of a commit the AI wrote.

Feature What it means Claude Code Cursor OpenCode 1.x OpenCode 2
Line attribution Each changed line of a commit is attributed to the AI or to a human, from a snapshot of each file before and after the agent edits it. Yes Partial Yes Yes
Shell changes attribution Changes that the agent makes with shell commands (for example sed) are attributed to the AI too, not only changes made with the file tools. Yes No Yes Yes

Why not Yes:

  • Line attribution, Cursor: Cursor has only the afterFileEdit hook, which gives the edits but no snapshot before the edit. The provider rebuilds the content before the edit from the edits.
  • Shell changes attribution, Cursor: Cursor has no hook before and after a shell command.

User experience

What the user sees in the agent.

Feature What it means Claude Code Cursor OpenCode 1.x OpenCode 2
Welcome message At session start, the user sees a message that tells that Chainloop records the session, and in which project. Yes No No No
Session link after push After a git push that the agent runs, the user sees a link to the session in Chainloop. Yes No No No

Why not Yes:

  • Welcome message, Cursor and OpenCode: the provider gives the session-start context to the model only. It has no channel that shows a message to the user at session start, so the welcome message is dropped.
  • Session link after push, Cursor: Cursor has no hook after a shell command, so there is no point at which to show the link.
  • Session link after push, OpenCode: the plugin hook fires after a shell command, but the response that shows a message to the user is not verified yet. The link stays pending until it expires.

Specs and skills

What the session was asked to build, and which skills it used.

Feature What it means Claude Code Cursor OpenCode 1.x OpenCode 2
Spec capture instruction At session start, the agent is told to capture the specs of the session (tickets, documents, plans, images) in the spec folder. A plan is captured only when it comes from an external source or from the plan mode of the agent: an agreement in the chat is not a plan. A pasted image is not captured, also when the agent has a file path for it. The agent tells the user in one line when it captures a file. Yes Yes Yes Yes
Spec capture reminder At each user prompt, the agent is reminded to capture new or changed specs. Yes No Yes Yes
Local spec sources read at push A capture whose source is a local file holds only a header and a placeholder. Each push reads the file and records its current content, so the evidence follows the edits of the file. Only a regular text file of up to 1 MiB is read. Otherwise the push keeps the body that the agent wrote, or drops the capture with a warning when there is no body (spec issue-3561). Yes Yes Yes Yes
Skills tracking The evidence lists the skills that the session used, with a copy of each skill as it ran. Yes No Partial Partial
Spec source pointers Exact copies of spec sources in the transcript (full file reads and file writes) are replaced with a pointer to the spec material, so the evidence does not hold the same content two or three times (spec issue-3556). Yes No No No
Pasted image pointers Each distinct image that the user pasted into the session, including images in prompts sent while the agent was busy, is stored once as an image spec entry, from the data in the transcript, with the title "Pasted image N" and no role. A repeated paste of the same image adds no entry. The inline image in the transcript is replaced with a pointer to that material. An image whose data cannot be decoded, or that the push could not store, stays inline, and the push continues. When the agent captured the same image, its entry is kept and no second entry is added (spec issue-3569). Yes No No No

Why not Yes:

  • Spec capture reminder, Cursor: Cursor has no hook at each user prompt. The agent gets the session-start instruction only.
  • Skills tracking, Cursor: the provider does not track skills. The evidence has no skill entries, which means "not recorded", not "no skill used".
  • Skills tracking, OpenCode: only the skills that the model starts with the skill tool are counted. Skills that the user starts, and skills used in subagents, are not.
  • Spec source pointers, Cursor and OpenCode: the provider has no finder for the copies in its transcript yet. For OpenCode, the rebuilt transcript holds no tool outputs, so only the write copies would apply.
  • Pasted image pointers, Cursor and OpenCode: the provider has no finder for pasted images yet. Each agent puts pasted images in a different block shape. The pasted images stay inline in the transcript. The agent is told not to capture them in the spec folder.

Security

How the evidence is protected before it leaves the machine.

Feature What it means Claude Code Cursor OpenCode 1.x OpenCode 2
Secret redaction Secrets in the session evidence and in the spec materials are replaced with a placeholder before upload. Yes Yes Yes Yes
Media skipped by redaction The secret redaction does not scan base64 media (images, PDFs) in the transcript, so it is faster and does not break the media. Yes Unknown Not applicable Not applicable

Why not Yes:

  • Media skipped by redaction, Cursor: the redaction skips base64 media blocks of the shape {"type":"base64","data":...}. The Cursor transcript is stored as it is, and it is not verified whether it holds media in that shape.
  • Media skipped by redaction, OpenCode: the rebuilt transcript holds no media.

Git integration

How the sessions are linked to the commits and attested.

Feature What it means Claude Code Cursor OpenCode 1.x OpenCode 2
Commit trailer Each commit gets a Chainloop-Trace-Sessions trailer with the sessions that contributed to it. Yes Yes Yes Yes
Attestation at push The pre-push hook attests the sessions of the pushed commits. Yes Yes Yes Yes

How each agent is hooked in

Agent Hook installation Hooks
Claude Code .claude/settings.json Session start and end, user prompt, and before and after each file and shell tool and the Skill tool.
Cursor .cursor/hooks.json Session start and end, and afterFileEdit. There is no shell hook and no prompt hook.
OpenCode .opencode/plugins/chainloop-trace.ts Session start and end, user prompt, and before and after each tool. One plugin covers both versions: server() for 1.x (1.3.4 or later) and setup() for 2. The provider picks the export command from opencode --version.

All agents share the git hooks commit-msg, post-commit and pre-push.

Documentation

Index

Constants

View Source
const (
	// AttributionAI marks file changes produced by an AI agent.
	AttributionAI = "ai"
	// AttributionHuman marks file changes produced by a human.
	AttributionHuman = "human"
)

Attribution constants for file-level code attribution.

View Source
const DefaultProviderName = "claude-code"

DefaultProviderName is the provider used when a SessionRecord predates the Provider field or when no explicit provider is selected. Kept here so both the providers registry and the action package can reference it without a package import.

Variables

View Source
var ErrAnnounceUnsupported = errors.New("agent cannot show messages to the user")

ErrAnnounceUnsupported is returned by AnnounceToUser when the agent has no channel for showing the user a message. It means nothing was displayed, as opposed to a delivery that was attempted and failed, so a caller holding single-use content can keep it rather than throw it away unseen.

Functions

func MustRawSessionEntry

func MustRawSessionEntry(e RawSessionEntry) json.RawMessage

MustRawSessionEntry marshals a RawSessionEntry to json.RawMessage. Panics only on impossible encoding failures (struct field bugs).

Types

type HookEdit

type HookEdit struct {
	// OldString is the text that was replaced.
	OldString string
	// NewString is the text that replaced OldString.
	NewString string
}

HookEdit represents a single old_string → new_string replacement applied to a file.

type HookInput

type HookInput struct {
	// SessionID is the agent-assigned identifier for the session.
	SessionID string `json:"session_id"`
	// HookEventName is the agent-specific event name (e.g., "PreToolUse", "afterFileEdit").
	HookEventName string `json:"hook_event_name,omitempty"`
	// ToolName is the name of the tool being invoked, if applicable.
	ToolName string `json:"tool_name,omitempty"`
	// FilePath is the absolute path of the file being edited, set by provider's ReadHookInput.
	FilePath string `json:"-"`
	// Cwd is the directory the agent session runs in, which is where the
	// agent files its transcripts. It can differ from the checkout that owns
	// FilePath, e.g. for an edit in a linked git worktree. Empty when the
	// agent does not report it.
	Cwd string `json:"cwd,omitempty"`
	// TranscriptPath is the path of the session transcript as the agent
	// reports it. Unlike Cwd, it does not move when the agent works in
	// another directory (e.g. a subagent in its own git worktree). Empty
	// when the agent does not report it.
	TranscriptPath string `json:"transcript_path,omitempty"`
	// AgentID identifies the subagent the hook fires in. Subagents share
	// their parent's SessionID, so this is what tells concurrent agents of
	// one session apart. Empty for the main agent.
	AgentID string `json:"agent_id,omitempty"`
	// ToolUseID is the agent's identifier for one tool call. Its pre and post
	// hooks carry the same value, so it tells overlapping calls of one agent
	// apart. Empty when the agent does not report it.
	ToolUseID string `json:"tool_use_id,omitempty"`
	// AgentVersion is the agent runtime version reported in the hook payload
	// (e.g., Cursor's cursor_version). Captured at session-start so parsing
	// can set Agent.Version even when the transcript itself doesn't carry it.
	AgentVersion string `json:"-"`
	// Model is the model identifier reported in the hook payload (e.g.,
	// Cursor's "model" field). Captured at session-start so parsing can
	// set Model.Primary even when the transcript itself doesn't carry it.
	Model string `json:"-"`
	// Edits carries per-file string replacements reported by the agent.
	// Providers that snapshot files pre-edit leave this empty; providers that
	// only emit post-edit events (e.g., Cursor's afterFileEdit) populate it so
	// consumers can reconstruct the "before" content via reverse application.
	Edits []HookEdit `json:"-"`
	// SkillDir is the folder of the skill that the tool call loaded, when the
	// hook payload gives it. Empty otherwise.
	SkillDir string `json:"-"`
	// ToolFailed reports that the hook fires after a tool call that failed
	// (Claude's PostToolUseFailure). The tool can still have changed files,
	// so its changes are recorded. But the agent expects a different hook
	// response for a failure, so nothing is written back to it.
	ToolFailed bool `json:"-"`
}

HookInput represents parsed hook invocation data from an AI agent.

type ParseOpts

type ParseOpts struct {
	// SessionDir is the directory holding the copied session transcript.
	SessionDir string
	// SessionID identifies the session to parse.
	SessionID string
	// AgentVersion is the runtime version captured at session-start (when
	// the agent reports it via hook payload). Providers whose transcripts
	// don't embed a version can use this to populate Agent.Version.
	AgentVersion string
	// Model is the model identifier captured at session-start. Providers
	// whose transcripts don't embed model info can use this to populate
	// Model.Primary.
	Model string
}

ParseOpts configures session parsing.

type PastedImage added in v1.120.0

type PastedImage struct {
	// Data is the decoded image, as the model got it.
	Data []byte
	// Digest is the digest of Data, in the form of pointer.Digest.
	Digest string
	// MediaType is the media type of the image, as the agent recorded it.
	MediaType string
	// Number is the number of the paste as the agent showed it to the user,
	// or the position of the image in the session, from 1, when the agent
	// showed no number.
	Number int
	// Timestamp is the time of the transcript entry that holds the image,
	// RFC3339 in UTC, or empty when the agent recorded none that can be read.
	Timestamp string
}

PastedImage is one image that the user pasted into a session.

type PastedImageFinder added in v1.120.0

type PastedImageFinder interface {
	// PastedImages returns each distinct image that the user pasted, in each
	// stream of the raw session: the main stream first, then the subagent
	// streams by name. An image pasted many times is returned one time, from
	// its first paste. An image whose data cannot be decoded is left out.
	PastedImages(raw map[string][]json.RawMessage) []PastedImage
}

PastedImageFinder is implemented by a provider that can find the images that the user pasted into its transcript (spec issue-3569). The push stores each one as a spec source, and ReplaceSpecCopies of the same provider then replaces it with a pointer. For the sessions of a provider without it, the pasted images stay inline.

type Provider

type Provider interface {
	// Name returns the agent identifier (e.g., "claude-code", "cursor").
	Name() string

	// ParseSession parses a session and returns structured evidence.
	ParseSession(ctx context.Context, opts *ParseOpts) (*aicodingsession.Evidence, error)

	// CopySessionData copies the agent's on-disk session artifacts into
	// the store's raw/ directory so pre-push can parse them independently of
	// the agent's own storage (which may be rotated/cleaned later).
	// loc says where the agent keeps the session's transcripts; it need not
	// be in the checkout that owns store.
	CopySessionData(store *state.Store, loc SessionLocation) error

	// CaptureFileSnapshot is invoked from the pre-edit hook to record any
	// state the provider needs to later reconstruct the file's pre-edit
	// content. Providers whose post-edit hook delivers the edit payload
	// directly (e.g. Cursor's afterFileEdit with old/new strings)
	// implement this as a no-op.
	CaptureFileSnapshot(store *state.Store, input *HookInput) error

	// ResolveBeforeContent reconstructs the file's content as it was
	// before the edit, given its current ("after") content. Returns nil
	// when no reconstruction is possible (no snapshot, no edits, or the
	// file was newly created); callers treat nil as "all lines are AI".
	ResolveBeforeContent(store *state.Store, input *HookInput, after []byte) []byte

	// CleanupAfterEdit releases any per-edit state captured in
	// CaptureFileSnapshot. Called once the post-edit handler is done with
	// the file, regardless of whether ranges were recorded.
	CleanupAfterEdit(store *state.Store, input *HookInput)

	// InstallHooks installs the agent's hooks in the repo (e.g., .claude/settings.json).
	InstallHooks(repoRoot string) error

	// InstallHooksForTraceRun installs the data-gathering subset of hooks
	// used by `chainloop trace run`. End-of-session hooks are omitted —
	// trace run drives attestation itself.
	InstallHooksForTraceRun(repoRoot string) error

	// SettingsFile returns the absolute path to the agent's on-disk
	// hooks/settings file (e.g. .claude/settings.json). Callers use it to
	// back up the file before install and restore it afterwards.
	SettingsFile(repoRoot string) string

	// UninstallHooks removes the agent's hooks from the repo.
	UninstallHooks(repoRoot string) error

	// HooksInstalled reports whether the repo already carries the agent's
	// Chainloop hooks, so `trace init` can offer the harnesses a repository
	// is set up for when it runs again. Hooks the user wrote do not count.
	HooksInstalled(repoRoot string) (bool, error)

	// ReadHookInput reads hook invocation input from the given reader.
	ReadHookInput(r io.Reader) (*HookInput, error)

	// IsFileWritingTool returns true if the named tool modifies files on disk.
	IsFileWritingTool(toolName string) bool

	// IsCommandTool returns true if the named tool runs a shell command (e.g.
	// Claude's "Bash"). Such tools can create or modify arbitrary files without
	// firing the file-writing hooks, so they are captured via a before/after
	// working-tree snapshot instead of a per-file snapshot.
	IsCommandTool(toolName string) bool

	// AnnounceSessionStart writes the session-start hook response to stdout,
	// as the one document the agent will read. A message with nothing on
	// either channel emits nothing, so the hook stays a no-op rather than
	// handing the agent an empty envelope to parse.
	AnnounceSessionStart(msg SessionStartMessage) error

	// SupportsSessionStartBanner reports whether the Banner of a message
	// handed to AnnounceSessionStart reaches the user rather than being
	// discarded. Callers check it before composing the banner, which costs a
	// control-plane round trip.
	SupportsSessionStartBanner() bool

	// SupportsSessionStartInstruction reports whether the Instruction of a
	// message handed to AnnounceSessionStart reaches the model rather than
	// being discarded.
	SupportsSessionStartInstruction() bool

	// AnnouncePromptSubmit writes the prompt-submit hook response to stdout,
	// so that reminder reaches the model's context for this turn. An empty
	// reminder emits nothing.
	AnnouncePromptSubmit(reminder string) error

	// SupportsPromptReminder reports whether a reminder handed to
	// AnnouncePromptSubmit reaches the model at each user prompt. An agent
	// without it gets the session-start instruction only.
	SupportsPromptReminder() bool

	// AnnounceToUser writes a hook response to stdout so the agent puts msg
	// in front of the user, after a shell command the agent ran. Which
	// channel that uses is the provider's business: agents differ in whether
	// they render text directly, relay it through the model, or both.
	//
	// Providers with no way to reach the user return ErrAnnounceUnsupported,
	// so callers can tell "shown" apart from "nothing happened" and avoid
	// discarding a message nobody saw.
	AnnounceToUser(msg string) error
}

Provider captures and parses AI coding sessions for a specific agent.

Providers are stateless singletons from a registry, so the state-touching methods below take a *state.Store per call rather than holding one. The store already knows where trace state lives — the .git directory inside a repository, or the out-of-tree directory `chainloop trace run` picks outside one — so implementations never reason about that distinction.

type RawSessionEntry

type RawSessionEntry struct {
	Type      string            `json:"type,omitempty"`
	Role      string            `json:"role,omitempty"`
	UUID      string            `json:"uuid,omitempty"`
	Timestamp string            `json:"timestamp,omitempty"`
	IsMeta    bool              `json:"isMeta,omitempty"`
	Message   RawSessionMessage `json:"message"`
}

RawSessionEntry is the wire format for a single message in raw_session["main"]. All providers must emit entries matching this struct so the frontend's ConversationTimeline can render them uniformly. The shape mirrors Claude's JSONL transcript format, which the frontend was originally built against.

type RawSessionMessage

type RawSessionMessage struct {
	Role    string          `json:"role,omitempty"`
	Content json.RawMessage `json:"content,omitempty"`
	Model   string          `json:"model,omitempty"`
}

RawSessionMessage is the message body inside a RawSessionEntry.

type RawSessionTextBlock

type RawSessionTextBlock struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

RawSessionTextBlock is a content block carrying plain text.

type RawSessionToolUseBlock

type RawSessionToolUseBlock struct {
	Type  string          `json:"type"`
	ID    string          `json:"id,omitempty"`
	Name  string          `json:"name,omitempty"`
	Input json.RawMessage `json:"input,omitempty"`
}

RawSessionToolUseBlock is a content block representing a tool call.

type SessionLocation added in v1.111.1

type SessionLocation struct {
	// SessionID identifies the session.
	SessionID string
	// Cwd is the directory the session runs in. Providers that key their
	// storage by directory use it when TranscriptPath is empty.
	Cwd string
	// TranscriptPath is the transcript path the agent reported, if any.
	// Providers that get one prefer it over Cwd.
	TranscriptPath string
}

SessionLocation tells a provider where to find a session's transcripts.

type SessionStartMessage added in v1.114.0

type SessionStartMessage struct {
	// Banner is shown to the user verbatim by the agent client, without
	// costing a model turn. Empty means nothing is shown.
	Banner string

	// Instruction is addressed to the model rather than the user: it reaches
	// the session's context so the agent can act on it. Empty means the model
	// is told nothing.
	Instruction string
}

SessionStartMessage is everything the session-start hook has to say, on the two channels an agent offers: one the user reads, one the model reads.

It is one value rather than two calls because an agent parses a hook's stdout as a single JSON document. A second write would be discarded, silently, together with whatever it carried — so the two channels are made inseparable here rather than left to each caller to remember.

func (SessionStartMessage) Empty added in v1.114.0

func (m SessionStartMessage) Empty() bool

Empty reports that there is nothing to deliver on either channel.

type SkillTracker added in v1.118.0

type SkillTracker interface {
	// NewSkillLoads returns the folders of the skills that the session loaded
	// since the last call for the session, as far as the hook can tell. The
	// caller copies each folder. It never fails: a hook that cannot tell
	// returns nothing, and the push copies the folder later.
	NewSkillLoads(store *state.Store, input *HookInput) []string

	// SessionSkills returns the skills that the session used, from the
	// session data under opts.SessionDir, in the order of their first use.
	// The warnings are for uses that could not be read.
	SessionSkills(opts *ParseOpts) ([]SkillUse, []string, error)

	// SkillRoots returns the folders that tell where a skill came from, for a
	// session in repoRoot.
	SkillRoots(repoRoot string) skill.Roots
}

SkillTracker is implemented by a provider that records the skills that a session used. A provider without it records no skills, and for its sessions no skill entry means "not recorded", not "no skill used".

type SkillUse added in v1.118.0

type SkillUse struct {
	// Name is the name that the agent used for the skill.
	Name string
	// Dir is the folder that the agent loaded the skill from, or "" when the
	// skill has no folder (a skill built into the agent) or the session data
	// does not tell. A use with no folder is not recorded.
	Dir string
	// FirstUsedAt is the time of the first use, RFC3339.
	FirstUsedAt string
	// ByModel counts the uses that the model started with a tool call.
	ByModel int
	// ByUser counts the uses that the user started with a slash command.
	ByUser int
	// InSubagents counts the uses, of either start, that came from subagents.
	InSubagents int
}

SkillUse is one skill that a session used, with the counts of its uses.

func SortedSkillUses added in v1.118.0

func SortedSkillUses(uses map[string]*SkillUse) []SkillUse

SortedSkillUses returns the uses in the order of their first use. The name breaks ties, so that the same session data always gives the same order: the limit on skill entries keeps the first ones.

func (*SkillUse) Merge added in v1.118.0

func (u *SkillUse) Merge(other SkillUse)

Merge adds the uses of other, of the same skill, to u.

func (*SkillUse) Record added in v1.118.0

func (u *SkillUse) Record(at string, byModel, subagent bool)

Record adds one use at time at, RFC3339, to the counts. The first use is the earliest time recorded, whatever the order of the calls.

type SpecCopyReplacer added in v1.119.0

type SpecCopyReplacer interface {
	// ReplaceSpecCopies replaces, in each stream of the raw session, each
	// exact copy of a source with a pointer to its material, and reports the
	// result per finder. It updates the lines in place and never fails: a
	// copy that does not match stays inline.
	ReplaceSpecCopies(raw map[string][]json.RawMessage, sources pointer.Sources) pointer.Report
}

SpecCopyReplacer is implemented by a provider that can find the copies of spec sources in its transcript (spec issue-3556). For the sessions of a provider without it, the transcript keeps its copies.

Directories

Path Synopsis
Package pointer holds the parts of the pointers to spec sources that do not depend on the agent (spec issue-3556).
Package pointer holds the parts of the pointers to spec sources that do not depend on the agent (spec issue-3556).
Package providers registers all available AI coding agent trace providers in one place.
Package providers registers all available AI coding agent trace providers in one place.
Package skill keeps a copy of each skill folder that a coding session used, and turns the copy into the materials that the evidence holds.
Package skill keeps a copy of each skill folder that a coding session used, and turns the copy into the materials that the evidence holds.
Package spec captures the specification a coding session was built from.
Package spec captures the specification a coding session was built from.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL