---
title: internal/session/cassette
description: Go API reference for the cassette package.
---
# cassette
```go
import "github.com/jbcom/radioactive-ralph/internal/session/cassette"
```
Package cassette provides a VCR\-style record/replay layer for the session wrapper. Cassettes capture the stdin \(user messages\) and stdout \(stream\-json frames\) of a real \`claude \-p\` subprocess so tests can replay the same conversation deterministically without needing API credentials.
The design is subprocess\-level rather than HTTP\-level: claude's actual API calls happen inside the subprocess and we never see the HTTP traffic. Cassettes therefore replay the observable I/O of claude itself, which is exactly the surface session.Session depends on.
Cassette usage flow:
1. Record once \(requires ANTHROPIC\_API\_KEY or authenticated Claude Code install\):
rec, \_ := cassette.NewRecorder\(cassettePath, realClaudeBin, args\) // rec exposes the same file descriptors Spawn uses // drive the session normally, rec writes the JSON cassette
2. Replay \(no auth required, runs in CI\):
opts.ClaudeBin = cassette.ReplayerPath os.Setenv\("RALPH\_CASSETTE\_PATH", cassettePath\) // session.Spawn exec's the replayer, which reads the cassette // and emits the recorded frames with recorded timing.
Cassettes are JSON so diffs in code review are meaningful.
## Index
- [Constants](<#constants>)
- [type Cassette](<#Cassette>)
- [func Load\(path string\) \(\*Cassette, error\)](<#Load>)
- [func \(c \*Cassette\) Save\(path string\) error](<#Cassette.Save>)
- [type Frame](<#Frame>)
- [type Recorder](<#Recorder>)
- [func NewRecorder\(ctx context.Context, cassettePath, bin string, args \[\]string\) \(\*Recorder, error\)](<#NewRecorder>)
- [func \(r \*Recorder\) Close\(\) error](<#Recorder.Close>)
- [func \(r \*Recorder\) Start\(\) error](<#Recorder.Start>)
- [func \(r \*Recorder\) Stdin\(\) io.WriteCloser](<#Recorder.Stdin>)
- [func \(r \*Recorder\) Stdout\(\) io.ReadCloser](<#Recorder.Stdout>)
- [func \(r \*Recorder\) Wait\(\) error](<#Recorder.Wait>)
## Constants
CurrentVersion is the schema version the recorder writes.
```go
const CurrentVersion = 1
```
## type [Cassette]()
Cassette is the top\-level on\-disk format.
```go
type Cassette struct {
// Version identifies the cassette schema. Bumped on
// incompatible changes.
Version int `json:"version"`
// RecordedAt is when the cassette was captured. Informational.
RecordedAt time.Time `json:"recorded_at"`
// ClaudeVersion is whatever `claude --version` reported when the
// cassette was recorded. Tests can warn (not fail) when the
// installed claude differs.
ClaudeVersion string `json:"claude_version,omitempty"`
// Args are the CLI args claude was invoked with at record time,
// minus --session-id (which the replayer must honor from the
// actual invocation).
Args []string `json:"args,omitempty"`
// Frames is the ordered stream of recorded events. Each frame is
// either an inbound frame from claude (stdin → stdout direction
// from the cassette's POV, which means stdout from the
// replayer's POV) or a user-input marker noting when stdin
// received a line from the client.
Frames []Frame `json:"frames"`
}
```
### func [Load]()
```go
func Load(path string) (*Cassette, error)
```
Load reads a cassette from disk.
### func \(\*Cassette\) [Save]()
```go
func (c *Cassette) Save(path string) error
```
Save writes c to path as indented JSON for readable diffs.
## type [Frame]()
Frame is one entry in a cassette.
```go
type Frame struct {
// Direction is "in" (stdin received from client) or "out" (stdout
// emitted to client). During replay, "in" frames act as
// checkpoints: the replayer waits for the client to send a
// matching user message before emitting subsequent "out" frames.
Direction string `json:"dir"`
// At is the offset from the session start when this frame was
// observed, in nanoseconds. Replayer enforces a minimum gap
// between consecutive "out" frames based on these offsets.
At time.Duration `json:"at_ns"`
// Line is the raw JSON line that was observed (no trailing \n).
// For "in" frames, this is the outbound stream-json object the
// client sent. For "out" frames, this is the inbound frame
// claude emitted.
Line json.RawMessage `json:"line"`
}
```
## type [Recorder]()
Recorder wraps a real claude subprocess and captures its I/O to a cassette. The public surface mirrors what session.Session needs: Stdin writer, Stdout reader, Start, Wait, Close.
Typical flow:
```
rec, err := NewRecorder(ctx, cassettePath, "claude", argv)
stdin := rec.Stdin()
stdout := rec.Stdout()
rec.Start() // spawns claude; I/O is passed through + taped
// ... drive session via stdin/stdout as usual ...
rec.Close() // flushes cassette to disk
```
```go
type Recorder struct {
// contains filtered or unexported fields
}
```
### func [NewRecorder]()
```go
func NewRecorder(ctx context.Context, cassettePath, bin string, args []string) (*Recorder, error)
```
NewRecorder returns an uninitialized Recorder. Callers must call Start before any I/O and Close after the session ends.
### func \(\*Recorder\) [Close]()
```go
func (r *Recorder) Close() error
```
Close stops the subprocess, flushes the cassette to disk, and returns any write error.
### func \(\*Recorder\) [Start]()
```go
func (r *Recorder) Start() error
```
Start spawns the claude subprocess. Returns any exec error.
### func \(\*Recorder\) [Stdin]()
```go
func (r *Recorder) Stdin() io.WriteCloser
```
Stdin returns the client\-side writer. Anything written here is forwarded to claude and recorded to the cassette.
### func \(\*Recorder\) [Stdout]()
```go
func (r *Recorder) Stdout() io.ReadCloser
```
Stdout returns the client\-side reader. Anything read here was emitted by claude \(and also recorded\).
### func \(\*Recorder\) [Wait]()
```go
func (r *Recorder) Wait() error
```
Wait waits for the subprocess to exit.
Generated by [gomarkdoc]()