Documentation
¶
Overview ¶
Package claudego drives Claude Code as a long-lived headless subprocess over bidirectional stream-json, on a Claude subscription.
The thesis: a Go program importing only this package holds a multi-turn session in which the model's only tools are functions defined in that program. The library spawns exactly one `claude` child per session and nothing else, hosts the caller's tools in-process over the control channel, asserts the granted tool surface at startup, and returns structured usage per turn. It never reads, forwards, or persists credentials — the spawned `claude` authenticates itself.
DESIGN.md (ratified 2026-08-09) is the doctrine home; the wire facts the library depends on are frozen under contracts/, pinned to the characterized `claude` release.
Example (AcceptanceShape) ¶
Example_acceptanceShape is the thesis as a runnable program: a caller importing only this library holds a multi-turn session in which the model's only tools are functions defined right here — the tool surface asserted at startup (I6), usage returned structurally per turn.
In the default battery it runs against the scripted fake claude (fixtures characterized from the real 2.1.226); the paid tier reruns the same shape against the real binary.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
claudego "github.com/Middlewatch/claude-go"
)
func main() {
adder := claudego.Tool{
Name: "add",
Description: "Add two integers and return their sum.",
InputSchema: json.RawMessage(`{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"]}`),
Handler: func(ctx context.Context, input json.RawMessage) (*claudego.ToolResult, error) {
var args struct{ A, B float64 }
if err := json.Unmarshal(input, &args); err != nil {
return nil, err // becomes an error-flagged result, not a session failure
}
return claudego.TextResult(fmt.Sprint(args.A + args.B)), nil
},
}
opts := claudego.Pipe("You are a terse calculator.", adder)
opts.ClaudePath = "tests/fake_claude.py" // the paid tier drops this line
opts.ToolServerName = "calc"
ctx := context.Background()
sess, err := claudego.Open(ctx, opts)
if err != nil {
log.Fatal(err)
}
defer sess.Close()
for _, prompt := range []string{"What is 2 plus 3?", "And 40 plus 2?"} {
res, err := sess.Prompt(ctx, prompt)
if err != nil {
log.Fatal(err)
}
fmt.Printf("turn: %q (tools granted: %v)\n", res.ResultText, sess.Init().Tools)
}
}
Output: turn: "5" (tools granted: [mcp__calc__add]) turn: "5" (tools granted: [mcp__calc__add])
Index ¶
- Variables
- type AssistantEvent
- type Boundary
- type ContextUsage
- type CumulativeUsage
- type DeltaKind
- type Event
- type InitEvent
- type MCPServerStatus
- type Options
- type PermissionDecision
- type PermissionFunc
- type PermissionRequest
- type Profile
- type ResultEvent
- type Session
- func (s *Session) Close() error
- func (s *Session) ContextUsage(ctx context.Context) (*ContextUsage, error)
- func (s *Session) Cumulative() CumulativeUsage
- func (s *Session) Events(ctx context.Context) iter.Seq2[Event, error]
- func (s *Session) Init() *InitEvent
- func (s *Session) Interrupt(ctx context.Context) error
- func (s *Session) Prompt(ctx context.Context, text string) (*ResultEvent, error)
- func (s *Session) Send(ctx context.Context, text string) error
- func (s *Session) SendRaw(ctx context.Context, userFrame []byte) error
- type StreamEvent
- type Tool
- type ToolHandler
- type ToolResult
- type UnknownEvent
- type Usage
- type UserEvent
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var EffortLevels = []string{"low", "medium", "high", "xhigh", "max"}
EffortLevels are the levels the pinned CLI accepts for --effort (`claude --help`, 2.1.226). Exported so a host can build a picker from the same list this package validates against.
var ErrAPIKeyAuth = errors.New("claudego: subscription auth intended but init reports an API key source")
ErrAPIKeyAuth reports that init named an API-key source. This library is a subscription lane: it fails loudly rather than silently billing a key. It also surfaces from the first turn, with the init snapshot.
var ErrSessionClosed = errors.New("claudego: session closed")
ErrSessionClosed reports an operation on a session after Close.
var ErrToolSurfaceMismatch = errors.New("claudego: system/init tools do not match the requested set")
ErrToolSurfaceMismatch reports that system/init advertised a tool set different from the requested one (I6). It surfaces from the first turn's Events/Prompt, since the wire only emits init after the first user message.
var ErrUnknownEffort = errors.New("claudego: unknown effort level")
ErrUnknownEffort reports an Options.Effort the pinned CLI does not accept. Checked before the spawn so the caller gets the valid set rather than a child exiting on a usage error.
Functions ¶
This section is empty.
Types ¶
type AssistantEvent ¶
type AssistantEvent struct {
Message json.RawMessage
ParentToolUseID string
// contains filtered or unexported fields
}
AssistantEvent carries an API-shape assistant message object, retained as raw bytes: message content is schema-fluid and callers that need blocks unmarshal what they use (I4).
func (*AssistantEvent) Raw ¶
func (e *AssistantEvent) Raw() []byte
type ContextUsage ¶
type ContextUsage struct {
Used int64
Max int64
Raw json.RawMessage // the full response, unknown fields retained (I4)
}
ContextUsage is the CLI's context-window accounting.
type CumulativeUsage ¶
type CumulativeUsage struct {
ByModel map[string]Usage
// TotalCostUSD is the CLI's client-side estimate (I7): useful for
// telemetry, not an invoice.
TotalCostUSD float64
Turns int
}
CumulativeUsage is the session's running total, read from the LATEST result event's modelUsage / total_cost_usd / num_turns — the wire's running totals, never summed across results (the falsification witness is TestCumulativeIsLatestNotSum). Per-model accounting is the source of truth (I7).
type DeltaKind ¶
type DeltaKind int
DeltaKind classifies the incremental text a StreamEvent carries.
type Event ¶
type Event interface {
// Raw returns the full frame bytes as read from the wire.
Raw() []byte
// contains filtered or unexported methods
}
Event is one decoded frame from the claude child's stdout stream. The set of concrete types is sealed; consume it with a type switch. Every event retains its full frame bytes (I4), so nothing the wire said is ever lost to decoding.
type InitEvent ¶
type InitEvent struct {
Model string
Tools []string
Capabilities []string // I5: the feature-detection surface
APIKeySource string
SessionID string
MCPServers []MCPServerStatus
// contains filtered or unexported fields
}
InitEvent is the system/init snapshot. Characterized timing (contracts/events.md): it arrives after every user message frame and never before the first one.
type MCPServerStatus ¶
MCPServerStatus is one entry of system/init's mcp_servers array.
type Options ¶
type Options struct {
ClaudePath string // claude binary; "" = "claude" from PATH
Dir string // child working directory; "" inherits
Env []string // extra KEY=VALUE appended to the parent env (I1: never credentials we read)
// CaptureDir, when set, records every HTTP request the child sends to
// the model API as one JSON file in that directory (credential headers
// redacted). Open points the child at a loopback proxy through
// ANTHROPIC_BASE_URL, forwarding to whatever base URL the environment
// already had; Close stops it. The wire is the only place the CLI's own
// system-prompt additions and per-turn reminders are visible.
CaptureDir string
Model string // --model; "" = CLI default
SystemPrompt string // delivered via the initialize control request
AppendSystemPrompt string
// BuiltinTools and SettingSources are tri-state (contracts/spawn-args.md):
// nil omits the flag entirely (CLI default set); an empty slice sends
// the empty form (no builtins / no setting sources).
BuiltinTools []string
SettingSources []string
// PermissionMode is passed through as --permission-mode. Characterized
// caution (2.1.226 capture): "dontAsk" DENIES tool calls without asking; a session
// whose tools must run wants "bypassPermissions" or an OnCanUseTool
// callback.
PermissionMode string // "", "default", "acceptEdits", "dontAsk", "plan", "bypassPermissions"
// Effort is the CLI's reasoning effort for the session (--effort).
// "" leaves the CLI's own default. Validated at Open: an unknown
// level is a caller bug worth catching before a spawn, not a child
// that exits with a usage error. Session-scoped by the CLI's own
// definition ("effort level for the current session"), so changing
// it means opening a new session.
Effort string // "", "low", "medium", "high", "xhigh", "max"
Tools []Tool // in-process tools, hosted over the control channel
ToolServerName string // MCP server name for Tools; "" = "claudego"
ExternalMCPServers json.RawMessage // raw mcpServers object, passed through to --mcp-config
StrictMCPConfig bool
IncludePartialMessages bool
MaxTurns int
AssertToolSurface bool // I6; Pipe sets true
// OnCanUseTool, when set, registers --permission-prompt-tool stdio so
// the CLI asks before every tool call. The callback path is
// characterized only under PermissionMode "default" (the captured
// configuration); combining it with bypassPermissions is
// uncharacterized wire territory.
OnCanUseTool PermissionFunc
ExtraArgs []string // appended verbatim; caller-owned escape hatch
}
Options configures a Session.
func Harness ¶
func Harness() Options
Harness is full Claude Code under program control: every knob at the CLI's own default.
func Pipe ¶
Pipe is the asserted floor profile: no builtin tools, no setting sources, strict MCP config, tool surface asserted at first init (I6), and bypassPermissions so the program's own tools actually run (the program is the permission layer; the pinned-CLI capture characterized dontAsk as deny-without-asking). Adjust fields on the returned value to taste.
type PermissionDecision ¶
type PermissionDecision struct {
Allow bool
UpdatedInput json.RawMessage // optional replacement input when allowed
Reason string // surfaced on denial
}
PermissionDecision is the caller's answer.
type PermissionFunc ¶
type PermissionFunc func(ctx context.Context, req *PermissionRequest) PermissionDecision
PermissionFunc answers a can_use_tool control request.
type PermissionRequest ¶
type PermissionRequest struct {
ToolName string
Input json.RawMessage
Raw json.RawMessage // full can_use_tool request, unknown fields retained (I4)
}
PermissionRequest is one tool-permission question from the CLI.
type Profile ¶
type Profile int
Profile names the two supported session postures. A Profile is a starting point produced by Pipe or Harness, not a mode switch: the constructors return an Options value the caller may adjust.
type ResultEvent ¶
type ResultEvent struct {
IsError bool
ResultText string
NumTurns int
DurationMS int64
TurnUsage Usage
ModelUsage map[string]Usage
TotalCostUSD float64
// contains filtered or unexported fields
}
ResultEvent marks the end of a turn.
TurnUsage is the result's `usage` field: main agent loop only, per turn. ModelUsage and TotalCostUSD are cumulative running totals across the session — read the latest result, never sum across results. TotalCostUSD is a client-side estimate (I7).
func (*ResultEvent) Raw ¶
func (e *ResultEvent) Raw() []byte
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is one live claude child and the stream conversation with it. A Session is not safe for concurrent use; the intended shape is one goroutine sending and ranging events.
func Open ¶
Open spawns one claude child (I3) speaking bidirectional stream-json, starts the read pump, and completes the initialize control exchange (systemPrompt, appendSystemPrompt, sdkMcpServers — the SDK-oracle path for the system prompt). Per the characterized timing (contracts/events.md), Open does NOT wait for system/init: the wire only emits it after the first user message, so the init snapshot and the I6 tool-surface assertion belong to the first turn.
ctx governs Open itself (bounded by a 30 s initialize deadline when it has none); the session's lifetime belongs to Close, so cancelling ctx after Open returns does not touch the child.
The caller must Close the returned session to reap the child.
func (*Session) Close ¶
Close ends the child's stdin and escalates through the oracle's teardown shape until the child is reaped: stdin EOF → 2 s grace → SIGTERM → 5 s grace → SIGKILL. Idempotent. A signal exit caused by our own escalation is a successful Close, not an error.
func (*Session) ContextUsage ¶
func (s *Session) ContextUsage(ctx context.Context) (*ContextUsage, error)
ContextUsage asks the CLI for its current context-window accounting via the get_context_usage control request.
func (*Session) Cumulative ¶
func (s *Session) Cumulative() CumulativeUsage
Cumulative returns the session's running usage totals as of the latest result event. The zero value (nil ByModel) means no result has carried totals yet.
func (*Session) Events ¶
Events yields decoded events from the current stream position until the session ends. Sequential re-ranging resumes where the previous range stopped; concurrent iteration is not supported.
func (*Session) Init ¶
Init returns the most recent system/init snapshot, or nil before the first turn: the wire emits init only after a user message (contracts/events.md), so a freshly opened session has none yet.
func (*Session) Interrupt ¶
Interrupt ends the in-flight turn via the control channel — never a signal (basis rule). On CLIs advertising interrupt_receipt_v1 (I5, characterized in contracts/events.md) the ack carries a receipt; older CLIs ack bare, and both are success. The interrupted turn still ends with its own error result on the event stream.
type StreamEvent ¶
type StreamEvent struct {
Inner json.RawMessage
// contains filtered or unexported fields
}
StreamEvent wraps one partial-message event (only with Options.IncludePartialMessages). Inner is the raw BetaRawMessageStreamEvent; Delta and Boundary give the two views callers actually consume.
func (*StreamEvent) Boundary ¶
func (e *StreamEvent) Boundary() Boundary
Boundary returns the structural edge this event marks, if any.
func (*StreamEvent) Delta ¶
func (e *StreamEvent) Delta() (DeltaKind, string)
Delta returns the incremental text this event carries, if any. Unrecognised delta types (signature_delta, future kinds) are DeltaNone.
func (*StreamEvent) Raw ¶
func (e *StreamEvent) Raw() []byte
type Tool ¶
type Tool struct {
Name string
Description string
InputSchema json.RawMessage // passed through byte-for-byte; nil = {"type":"object"}
Handler ToolHandler
}
Tool is one caller-defined tool, hosted in-process and exposed to the model as mcp__<server>__<name> (server defaults to "claudego").
type ToolHandler ¶
type ToolHandler func(ctx context.Context, input json.RawMessage) (*ToolResult, error)
ToolHandler runs one tool call. A returned error becomes an error-flagged tool result — it never fails the session.
There is no library-side per-call timeout: the CLI owns tool-call deadlines (its MCP_TOOL_TIMEOUT env var, effectively unbounded by default). ctx is cancelled when the CLI cancels the call or the session closes; a handler that ignores ctx parks its goroutine until it returns — honor ctx in anything long-running.
type ToolResult ¶
type ToolResult struct {
Content json.RawMessage // MCP content array
IsError bool // flows to the model as data
}
ToolResult is what the model sees back.
func ErrorResult ¶
func ErrorResult(text string) *ToolResult
ErrorResult wraps text as an error-flagged tool result.
func TextResult ¶
func TextResult(text string) *ToolResult
TextResult wraps text as a single-block tool result.
type UnknownEvent ¶
UnknownEvent is any frame the decoder does not type. It is data, never an error: the event schema moves roughly 25 releases a month, and a decoder that fails on an unrecognised frame breaks in production within weeks (I4).
func (*UnknownEvent) Raw ¶
func (e *UnknownEvent) Raw() []byte
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
claude-go
command
The bridge subcommand: stdio in, stdio out, one session behind it (contracts/bridge-v1.md).
|
The bridge subcommand: stdio in, stdio out, one session behind it (contracts/bridge-v1.md). |
|
internal
|
|
|
bridge
Package bridge implements bridge protocol v1 (contracts/bridge-v1.md): the NDJSON stdio seam that lets a non-Go host hold a claudego session while all hard logic — transcript prefix-matching, restart decisions, proxy-tool bookkeeping — stays on this side, keeping every host thin.
|
Package bridge implements bridge protocol v1 (contracts/bridge-v1.md): the NDJSON stdio seam that lets a non-Go host hold a claudego session while all hard logic — transcript prefix-matching, restart decisions, proxy-tool bookkeeping — stays on this side, keeping every host thin. |
|
control
Package control implements the stream-json control channel: the request/response frames that ride the same stdio pipes as events.
|
Package control implements the stream-json control channel: the request/response frames that ride the same stdio pipes as events. |
|
toolhost
Package toolhost answers the MCP dialect Claude Code speaks to in-process ("sdk") tool servers over the control channel's mcp_message seam.
|
Package toolhost answers the MCP dialect Claude Code speaks to in-process ("sdk") tool servers over the control channel's mcp_message seam. |
|
wiretap
Package wiretap records what the claude child actually sends to the model API.
|
Package wiretap records what the claude child actually sends to the model API. |
|
Package streamjson frames the newline-delimited JSON that Claude Code emits under --output-format stream-json and accepts under --input-format stream-json.
|
Package streamjson frames the newline-delimited JSON that Claude Code emits under --output-format stream-json and accepts under --input-format stream-json. |