Go SDK for building agentic applications with the Codex CLI. It provides:
Query()for one-shot requestsQueryStream()for multi-message streaming inputClientfor stateful multi-turn sessions
- Go 1.26+
- Codex CLI v0.103.0+ in
PATH
Compatibility constants are defined in version.go:
VersionMinimumCLIVersion
go get github.com/ethpandaops/codex-agent-sdk-gopackage main
import (
"context"
"fmt"
"time"
codexsdk "github.com/ethpandaops/codex-agent-sdk-go"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
for msg, err := range codexsdk.Query(ctx, "What is 2 + 2?") {
if err != nil {
fmt.Printf("query error: %v\n", err)
return
}
switch m := msg.(type) {
case *codexsdk.AssistantMessage:
for _, block := range m.Content {
if text, ok := block.(*codexsdk.TextBlock); ok {
fmt.Println(text.Text)
}
}
case *codexsdk.ResultMessage:
if m.Usage != nil {
fmt.Printf("done — %d input, %d output tokens\n", m.Usage.InputTokens, m.Usage.OutputTokens)
}
}
}
}client := codexsdk.NewClient()
defer func() {
if err := client.Close(); err != nil {
fmt.Fprintf(os.Stderr, "failed to close client: %v\n", err)
}
}()
if err := client.Start(ctx,
codexsdk.WithLogger(slog.Default()),
codexsdk.WithPermissionMode("acceptEdits"),
); err != nil {
return err
}
if err := client.Query(ctx, "What's the capital of France?"); err != nil {
return err
}
for msg, err := range client.ReceiveResponse(ctx) {
if err != nil {
return err
}
_ = msg
}
if err := client.Query(ctx, "What's the population of that city?"); err != nil {
return err
}
for msg, err := range client.ReceiveResponse(ctx) {
if err != nil {
return err
}
_ = msg
}err := codexsdk.WithClient(ctx, func(c codexsdk.Client) error {
if err := c.Query(ctx, "Hello Codex"); err != nil {
return err
}
for msg, err := range c.ReceiveResponse(ctx) {
if err != nil {
return err
}
_ = msg
}
return nil
},
codexsdk.WithLogger(slog.Default()),
codexsdk.WithPermissionMode("acceptEdits"),
)
if err != nil {
return err
}| API | Description |
|---|---|
Query(ctx, prompt, opts...) |
One-shot query returning iter.Seq2[Message, error] |
QueryStream(ctx, messages, opts...) |
Streams StreamingMessage input and yields Message output |
ListModels(ctx, opts...) |
Returns the complete discovered model list across all CLI pages |
ListModelsResponse(ctx, opts...) |
Returns the complete discovered model-list payload across all CLI pages |
NewClient() |
Creates a stateful client for interactive sessions |
WithClient(ctx, fn, opts...) |
Helper that runs Start() + callback + Close() |
StatSession(ctx, sessionID, opts...) |
Read session metadata from local SQLite database |
ListSessions(ctx, opts...) |
List persisted local sessions from the Codex state database |
GetSessionMessages(ctx, sessionID, opts...) |
Read parsed persisted rollout messages, including task lifecycle events |
| Method | Description |
|---|---|
Start(ctx, opts...) |
Connect and initialize a session |
StartWithPrompt(ctx, prompt, opts...) |
Start and immediately send first prompt |
StartWithStream(ctx, messages, opts...) |
Start and immediately stream input messages |
Query(ctx, prompt, sessionID...) |
Send a user turn |
ReceiveResponse(ctx) |
Read messages until ResultMessage |
ReceiveMessages(ctx) |
Continuous message stream |
Interrupt(ctx) |
Stop in-flight generation |
SetPermissionMode(ctx, mode) |
Change permission mode during a session |
SetModel(ctx, model) |
Change model during a session |
GetMCPStatus(ctx) |
Fetch live MCP server connection status |
ListModels(ctx) |
Fetch the complete discovered model list across all CLI pages |
ListModelsResponse(ctx) |
Fetch the complete discovered model-list payload across all CLI pages |
RewindFiles(ctx, userMessageID) |
Rewind tracked files to earlier turn |
Close() |
Close and release resources |
- Message types:
AssistantMessage,UserMessage,SystemMessage,TaskStartedMessage,TaskCompleteMessage,ThreadRolledBackMessage,ResultMessage,StreamEvent - Content blocks:
TextBlock,ThinkingBlock,ToolUseBlock,ToolResultBlock
GetSessionMessages returns the typed persisted rollout events as recorded on disk. Task lifecycle records remain TaskStartedMessage and TaskCompleteMessage; they are not converted into ResultMessage.
ListModels returns the fully aggregated model list across all internal model/list pages. ListModelsResponse returns the same complete model set plus response metadata. Extra provider-specific model/list fields are preserved in ModelInfo.Metadata. The SDK also decorates known models when Codex does not return them directly:
metadata.modelContextWindowmetadata.modelContextWindowSource(cli,official, orruntime)metadata.maxOutputTokensmetadata.maxOutputTokensSource(cliorofficial)
| Helper | Description |
|---|---|
SingleMessage(content) |
Single-message input stream |
MessagesFromSlice(msgs) |
Stream from []StreamingMessage |
MessagesFromChannel(ch) |
Stream from channel |
NewUserMessage(content) |
Convenience StreamingMessage constructor |
Options are backend-dependent. Unsupported combinations fail fast with ErrUnsupportedOption.
Query(...)auto-selects backend:- Uses
execwhen all selected options are natively supported there. - Falls back to
app-serverwhen needed for selected options.
- Uses
QueryStream(...)uses app-server semantics unless you inject a customWithTransport(...).Client.Start(...)uses app-server transport semantics.
| Option | Purpose |
|---|---|
WithLogger(logger) |
SDK logs (*slog.Logger) |
WithModel("gpt-5.4") |
Model selection |
WithCwd("/path") / WithCliPath("/path/codex") / WithEnv(...) |
Process/runtime setup |
WithPermissionMode("acceptEdits") / WithSandbox("workspace-write") |
Permission/sandbox behavior |
WithSystemPrompt("...") / WithSystemPromptPreset(...) |
System instructions |
WithImages(...) / WithConfig(...) |
Codex-native CLI inputs/config |
WithOutputSchema(json) |
Passes --output-schema |
WithOutputFormat(map[string]any{...}) |
Structured output wrapper/schema for app-server flow |
WithSkipVersionCheck(true) |
Skip CLI version check |
WithInitializeTimeout(d) |
Initialize control request timeout |
WithStderr(func(string){...}) |
Stderr callback |
WithTransport(customTransport) |
Custom transport injection |
| Option | Purpose |
|---|---|
WithHooks(hooks) |
Hook callbacks for tool/session events |
WithCanUseTool(callback) |
Per-tool permission callback |
WithOnUserInput(callback) |
Handle agent's requestUserInput questions (plan mode) |
WithTools(...) / WithAllowedTools(...) / WithDisallowedTools(...) |
Tool allow/block policy |
WithPermissionPromptToolName("stdio") |
Permission prompt tool name |
WithMCPServers(...) |
Register MCP servers |
WithSDKTools(tools...) |
Register SDK-defined tools via NewTool (uses dynamicTools API) |
| Option | Purpose |
|---|---|
WithResume("session-id") / WithForkSession(true) |
Resume/fork sessions |
WithContinueConversation(true) |
Continue prior conversation |
WithEffort(codexsdk.EffortHigh) |
Extended thinking effort |
WithIncludePartialMessages(true) |
Emit streaming deltas as StreamEvent |
WithAddDirs("/extra/path") |
Additional accessible directories |
WithExtraArgs(map[string]*string{...}) |
Raw CLI flags |
WithCodexHome("/path/.codex") |
Override Codex home directory for StatSession |
WithContinueConversation(true)requiresWithResume(...)on app-server paths.WithPermissionPromptToolName(...)only supports"stdio"on app-server paths.WithAddDirs(...)andWithExtraArgs(...)are unsupported on app-server paths.WithOutputSchema(...)andWithOutputFormat(...)serve different integration styles; choose one based on how you want structured output surfaced.
Read metadata from a local Codex session using StatSession:
stat, err := codexsdk.StatSession(ctx, "550e8400-e29b-41d4-a716-446655440000",
codexsdk.WithCodexHome("/custom/.codex"), // optional
codexsdk.WithCwd("/home/user/project"), // optional: filter by project
)
if err != nil {
if errors.Is(err, codexsdk.ErrSessionNotFound) {
log.Fatal("session not found")
}
log.Fatal(err)
}
fmt.Printf("Session: %s (tokens: %d)\n", stat.Title, stat.TokensUsed)List sessions or read persisted rollout messages:
sessions, err := codexsdk.ListSessions(ctx,
codexsdk.WithCodexHome("/custom/.codex"), // optional
codexsdk.WithCwd("/home/user/project"), // optional: filter by project
)
if err != nil {
log.Fatal(err)
}
messages, err := codexsdk.GetSessionMessages(ctx, sessions[0].SessionID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Loaded %d messages from persisted rollout\n", len(messages))StatSession reads from the Codex CLI's local SQLite database (~/.codex/state_5.sqlite).
No running CLI instance is required.
for msg, err := range codexsdk.Query(ctx, prompt) {
if err != nil {
var cliErr *codexsdk.CLINotFoundError
if errors.As(err, &cliErr) {
log.Fatalf("codex CLI not found: %v", cliErr.SearchedPaths)
}
var procErr *codexsdk.ProcessError
if errors.As(err, &procErr) {
log.Fatalf("codex failed (exit %d): %s", procErr.ExitCode, procErr.Stderr)
}
if errors.Is(err, codexsdk.ErrUnsupportedOption) {
log.Fatalf("unsupported option combination: %v", err)
}
log.Fatal(err)
}
_ = msg
}| Error | Description |
|---|---|
CLINotFoundError |
Codex CLI binary not found |
CLIConnectionError |
Connection/init failure |
ProcessError |
CLI process exited with error |
MessageParseError |
Failed to parse SDK message payload |
CLIJSONDecodeError |
JSON decode failure from CLI output |
ErrUnsupportedOption |
Option/backend combination is unsupported |
ErrSessionNotFound |
Session not found in local database |
| Example | Description |
|---|---|
quick_start |
Basic Query() usage |
client_multi_turn |
Stateful multi-turn client patterns |
query_stream |
QueryStream() with streaming inputs |
mcp_calculator |
In-process MCP server tools |
mcp_status |
Querying MCP server status |
tool_permission_callback |
WithCanUseTool permission callback |
user_input_callback |
WithOnUserInput user input callback (plan mode) |
tools_option |
Tool allow/block configuration |
structured_output |
Structured output patterns |
extended_thinking |
WithEffort(...) usage |
sessions |
Resume/fork session behavior |
parallel_queries |
Concurrent one-shot queries |
pipeline |
Multi-step orchestration flow |
sdk_tools |
SDK-defined tools with WithSDKTools |
include_partial_messages |
Real-time streaming deltas with WithIncludePartialMessages |
error_handling |
Typed error handling |
Run examples:
go run ./examples/quick_start
go run ./examples/client_multi_turn basic_streaming
go run ./examples/query_streamgo build ./...
go test ./...
go test -race ./...
go test -tags=integration ./... # requires Codex CLI + working runtime environment
golangci-lint run