Embed the Codex app-server in Go workflows.
This SDK speaks JSON-RPC to the codex app-server process. By default it spawns the CLI and communicates over stdio.
- Go 1.26.9 or newer
codexavailable on yourPATH
go get github.com/pmenglund/codex-sdk-gopackage main
import (
"context"
"fmt"
"log/slog"
"os"
"github.com/pmenglund/codex-sdk-go"
)
func main() {
ctx := context.Background()
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
prompt := "Diagnose the test failure and propose a fix"
client, err := codex.New(ctx, codex.Options{Logger: logger})
if err != nil {
panic(err)
}
defer client.Close()
thread, err := client.StartThread(ctx, codex.ThreadStartOptions{})
if err != nil {
panic(err)
}
result, err := thread.Run(ctx, prompt, nil)
if err != nil {
panic(err)
}
fmt.Println(result.FinalResponse)
}New uses its context.Context for initialization requests (initialize/initialized).
After New returns successfully, the spawned app-server lifetime is managed by Close, so canceling the constructor context later does not terminate the process.
Use RunStreamed to receive notifications as the turn progresses.
prompt := "Inspect the repo"
stream, err := thread.RunStreamed(ctx, []codex.Input{codex.TextInput(prompt)}, nil)
if err != nil {
panic(err)
}
defer stream.Close()
for {
note, err := stream.Next(ctx)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", note.Method)
if note.Method == "turn/failed" {
panic("turn failed")
}
if note.Method == "turn/completed" {
if completed, ok := note.Params.(protocol.TurnCompletedNotification); ok &&
completed.Turn != nil && completed.Turn.Status == protocol.TurnStatusFailed {
panic(fmt.Errorf("turn failed: %v", completed.Turn.Error))
}
break
}
}RunStreamed returns thread-scoped events plus notifications that omit threadId (for example account/session updates) so global events are not silently dropped.
Use StartTurn when you need to steer or interrupt a running turn.
handle, err := thread.StartTurn(ctx, []codex.Input{codex.TextInput("Inspect the repo")}, nil)
if err != nil {
panic(err)
}
if _, err := handle.Steer(ctx, []codex.Input{codex.TextInput("Focus on tests")}); err != nil {
panic(err)
}
result, err := handle.Run(ctx)
if err != nil {
panic(err)
}
fmt.Println(result.FinalResponse)TurnHandle owns its notification subscription. Call Close if you stop before Run returns.
Choose exactly one consumption style per handle: Run, repeated Next calls,
or the stream returned by Stream. Mixing styles or consuming concurrently
returns codex.ErrTurnConsumptionMode immediately.
If a turn fails, Run returns both the partial result and a *codex.TurnError.
Use errors.Is(err, codex.ErrTurnFailed) to classify the failure and
errors.As to inspect its structured details, retry metadata, and raw payload:
result, err := handle.Run(ctx)
if errors.Is(err, codex.ErrTurnFailed) {
var turnErr *codex.TurnError
if errors.As(err, &turnErr) {
fmt.Printf("received %d items before failure: %v\n", len(result.Items), turnErr.Detail)
}
}FinalResponse is populated only by completed agentMessage items. Plan,
reasoning, and commentary items remain available in Items but are never
reported as the final assistant answer.
Canceling or expiring the context passed to Run, RunInputs, or
TurnHandle.Run best-effort interrupts the remote turn before returning the
original context error. Cleanup is bounded to two seconds. To detach without
interrupting remote work, use RunStreamed (or StartTurn plus manual Next)
and close only the local stream or handle.
Low-level rpc.Client.SubscribeNotifications buffers at most the requested number
of pending notifications (64 by default). If a consumer falls behind, only that
iterator closes and Next returns an rpc.NotificationOverflowError; JSON-RPC
responses and other subscribers continue normally.
note, err := iterator.Next(ctx)
var overflow *rpc.NotificationOverflowError
if errors.As(err, &overflow) {
// Events were lost. Increase the capacity, drain faster, and subscribe
// again; overflow.Capacity reports the exhausted hard limit.
}High-level helpers wrap common app-server operations without requiring direct JSON-RPC calls.
account, err := client.Account(ctx, codex.AccountOptions{})
models, err := client.ListModels(ctx, codex.ListModelsOptions{})
threads, err := client.ListThreads(ctx, codex.ThreadListOptions{})Thread list, read, fork, and unarchive helpers return concrete protocol values.
List sorting uses protocol.SortDirection and protocol.ThreadSortKey rather
than arbitrary strings.
Thread values also expose lifecycle helpers:
if _, err := thread.SetName(ctx, "Investigation"); err != nil {
panic(err)
}
forked, _, err := thread.Fork(ctx, codex.ThreadForkOptions{})
if err != nil {
panic(err)
}
_ = forkedFor lower-level or less stable protocol features, use client.Client() and the generated rpc package.
Discriminated protocol unions are concrete generated wrapper types rather than
interface{}. Construct values with functions such as
protocol.NewUserInput, inspect the typed Kind, and use RawJSON when a
variant-specific payload is needed:
input, err := protocol.NewUserInput(map[string]any{
"type": "text",
"text": "Inspect the repository",
})
if err != nil {
return err
}
if input.Kind() == protocol.UserInputKindText {
var payload struct {
Text string `json:"text"`
}
if err := json.Unmarshal(input.RawJSON(), &payload); err != nil {
return err
}
}Constructors validate JSON encoding, the required non-empty discriminator, and
required-field presence for known variants. Field value types and other schema
constraints remain server-validated. Known variants have generated kind
constants; a well-formed future discriminator remains round-trippable and
reports IsKnown() == false. Existing opaque schemas remain interface{} only
through reviewed generator allowlists, and generation fails when a new weak
fallback appears. The 31 affected wrappers—including UserInput,
ResponseItem, SandboxPolicy, ThreadStatus, and LoginAccountParams—are
listed in protocol/unions_gen.go.
Generated protocol declarations use Go initialisms consistently, including
MCP, OAuth, ID, URL, and JSON. Older Mcp.../Oauth... spellings and
Sanitized...JSON implementation names remain deprecated aliases for a
migration window. Prefer canonical declarations such as
protocol.MCPServerOAuthLoginParams and canonical methods such as
rpc.Client.MCPServerOAuthLogin.
These API changes are currently unreleased and require the first SDK release
after v0.145.0; protocol.GeneratedCodexVersion continues to describe the
upstream wire schema, not the SDK release number. This API-hardening work also
includes Options.CompatibilityPolicy and changes StartLogin to accept
JSON-marshalable login parameters; code using unkeyed Options literals,
interface method sets, or assigned StartLogin method values must be updated.
Configure app-server requests with optional callbacks. Unset callbacks return
rpc.ErrServerRequestUnsupported, so adding a future server request does not
break existing applications at compile time.
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
client, err := codex.New(ctx, codex.Options{
Logger: logger,
RequestHandler: codex.RejectingApprovalCallbacks(),
})For custom logic, set only the callbacks your application supports:
callbacks := &codex.ServerRequestCallbacks{
ApproveFileChange: func(ctx context.Context, request protocol.FileChangeRequestApprovalParams) (*protocol.FileChangeRequestApprovalResponse, error) {
return &protocol.FileChangeRequestApprovalResponse{
Decision: protocol.FileChangeApprovalDecisionDecline,
}, nil
},
}
client, err := codex.New(ctx, codex.Options{RequestHandler: callbacks})Options.ApprovalHandler and the broad generated rpc.ServerRequestHandler
remain as deprecated compatibility surfaces. Low-level handlers that must
implement only selected methods can embed rpc.UnimplementedServerRequestHandler.
For methods whose historical Go spelling used non-canonical initialisms, such
as McpServerElicitationRequest, implement the exported canonical capability
interface (for example, rpc.MCPServerElicitationRequestHandler); dispatch
prefers that method over the legacy spelling. The client may invoke different
server-request callbacks concurrently, so callbacks that share state must be
concurrency-safe.
AutoApproveHandler accepts command, file-change, and permission requests and
must only be used in a trusted environment. Its default logs are redacted. If
sensitive command and path logging is explicitly required, construct
NewUnsafeLoggingAutoApproveHandler(logger) and protect those logs accordingly.
Use codex.AutoApproveCallbacks(codex.AutoApproveHandler{Logger: logger}) to
attach a safe auto-approver through the preferred callback API. The unsafe
handler can be passed to the same adapter when explicitly required.
When the SDK spawns codex, it requires the CLI major/minor version to match
the generated protocol version; patch differences are accepted. A mismatch,
missing binary, or unparseable version returns *codex.CodexCompatibilityError
before the process starts. Set CompatibilityPolicy: codex.Warn only after
validating compatibility (and provide a logger to see the warning), or
codex.Ignore to skip the probe. Custom transports are never probed.
This SDK targets Codex CLI 0.162.x. Low-level RPC adds
ThreadAttachmentOwnerList for reverse attachment lookup and
thread/prediction/updated notifications with raw-preserving completed/failed
results. TurnStartParams.ParentTurnID and RootTurnID attribute delegated work;
leave them unset for work started directly by the user. Returned Turn values
expose nullable RootTurnID.
Goal set/clear parameters now accept optional Origin (user or automatic).
Missing origin does not supply user authorization. MCP OAuth responses and
completion notifications expose optional LoginID for matching login attempts.
Assistant message phases now include partial_answer, which may precede more
output or tools.
CodexErrorInfo is now an alias of json.RawMessage, preserving arbitrary
future error objects and large integers. Callers assigning strings or maps must
marshal those values first; use json.Unmarshal to inspect the received JSON.
ResponseItem recognizes additional_tools and preserves its arbitrary tool
payloads. Upstream removed ModelProviderCapabilitiesReadResponse.NamespaceTools;
that field is removed from the generated SDK type.
The 0.160.0 update added gateway OAuth status,
login, and cancellation, plus thread attachment add/list/remove calls and their
notifications. Attachment payloads use json.RawMessage to preserve arbitrary JSON
numbers and future fields. Upstream removed thread/rollback; its generated method and
types are removed. Use ThreadRevert with an explicit BeforeTurnID instead.
History pagination still accepts ThreadItemsListParams.Cursor as a string
pointer. For an exclusive item position, construct a
protocol.ThreadItemsListAnchor with protocol.NewThreadItemsListAnchor, set
CursorAnchor, and provide a nonempty TurnID. Supplying both cursor forms is
an error. History entries expose optional StartedAtMs and CompletedAtMs.
Image inputs and function outputs accept either a URL or an uploaded file ID
through the existing raw-preserving union constructors. At least one image
source is required. Lifecycle responses retain DisabledPluginIDs, and resume
responses expose CollaborationMode. In low-level TurnStartParams, a nil
DisabledPluginIDs preserves the saved list; a pointer to an empty slice clears
it. These saved plugin IDs do not yet filter plugin capabilities upstream.
MCP tool calls expose descriptor UI metadata, and resource reads can select an
explicit app/account target. Its nullable LinkID is required on the wire;
null requests no-auth access subject to the app's policy. Personality selection
is deprecated upstream: friendly and pragmatic no longer select a style.
ConfigRequirements.WindowsSandboxPrivateDesktop and its helper type are
removed upstream. AllowedWindowsSandboxImplementations now uses
WindowsSandboxImplementation, which includes mxc, instead of
WindowsSandboxSetupMode.
Protocol 0.154.0 introduced the following additions. AccountRateLimitsRead(ctx) remains
available; low-level AccountRateLimitsReadWithParams accepts optional
protocol.GetAccountRateLimitsParams capabilities for Luna Reserve and
background reads that exclude reset-credit details. Rate-limit responses expose
nullable OrdinaryUsageAllowed and quota NormalModelSlug values. A missing
ordinary-usage permission does not imply that usage has recovered.
protocol.Thread.Originator identifies the client that created a thread.
ThreadListOptions.Originators filters by exact originator on hosted backends;
the local app-server rejects nonempty originator filters. Nil or empty filters
leave originators unrestricted.
The protocol also adds the configuration_update response-item variant,
application network requirement types, and MCP tool-discovery errors. Detached
review delivery is deprecated upstream; start a separate thread and request an
inline review instead.
Protocol 0.153.4 introduced low-level RPC for paginated turn and item history, thread revert, plugin reconciliation, and new realtime, project, queue, and authentication-recovery notifications.
AccountUsageRead(ctx) remains available. Use
AccountUsageReadWithParams(ctx, &protocol.GetAccountTokenUsageParams{ThreadID: &id})
for thread-specific usage. ThreadForkOptions.ExcludeTurns is supported again.
Low-level resume and turn-start parameters expose the new history and per-turn
options. Thread.ProjectID is nullable and always serializes as projectId, as
required by the new protocol. Approval requests that omit kind decode as
command, matching the upstream default for older servers.
Section appearance updates distinguish omission, clearing, and replacement:
leave ThreadSectionUpdateParams.Appearance nil to preserve it, assign a pointer
to a nil *protocol.ThreadSectionAppearance to clear it, or point to an appearance
value to replace it. Existing section response names remain available.
CapabilityRootLocation, ClientNotification, DynamicToolNamespaceTool,
LocalShellAction, and ReasoningItemReasoningSummary now use the same raw-preserving union wrappers as
the other discriminated types. Replace direct map assignments with the
corresponding protocol.New<Type>(value) constructor, and use RawJSON() to
inspect the payload. Known variants validate shared required fields; unknown
future variants continue to round-trip unchanged.
Provide a JSON Schema to constrain the final assistant message.
prompt := "Summarize repo status"
schema := codex.MustJSON(map[string]any{
"type": "object",
"properties": map[string]any{
"summary": map[string]any{"type": "string"},
"status": map[string]any{"type": "string", "enum": []string{"ok", "action_required"}},
},
"required": []string{"summary", "status"},
"additionalProperties": false,
})
_, err := thread.RunInputs(ctx, []codex.Input{codex.TextInput(prompt)}, &codex.TurnOptions{
OutputSchema: schema,
})Fields like ApprovalPolicy, SandboxPolicy, Effort, Summary, and OutputSchema accept any JSON-marshalable value. If you already have raw JSON, pass a json.RawMessage (or codex.MustJSON(...)) to avoid double encoding.
For common values, prefer typed constants from this package:
codex.ApprovalPolicyNever,codex.ApprovalPolicyOnFailure,codex.ApprovalPolicyOnRequest,codex.ApprovalPolicyUntrustedcodex.SandboxModeReadOnly,codex.SandboxModeWorkspaceWrite,codex.SandboxModeDangerFullAccesscodex.ReasoningEffortNone,codex.ReasoningEffortMinimal,codex.ReasoningEffortLow,codex.ReasoningEffortMedium,codex.ReasoningEffortHigh,codex.ReasoningEffortXHigh
Use helpers to build structured inputs:
inputs := []codex.Input{
codex.TextInput("Inspect this file"),
codex.MentionInput("AGENTS.md"),
}Overload classification uses ordinary Go errors:
if codex.IsOverloaded(err) && operationIsIdempotent {
// Retry according to your caller policy.
}IsOverloaded classifies structured overload failures; it does not prove that
repeating an operation is safe. Base retries on idempotency or an
application-level idempotency key. IsRetryable remains as a deprecated alias
for source compatibility.
Use the RPC client directly for full control.
rpcClient := client.Client()
models, err := rpcClient.ModelList(ctx, protocol.ModelListParams{})Use checked constructors such as rpc.NewClientChecked,
rpc.NewConnTransportChecked, and rpc.NewReplayTransportChecked when a
dependency is dynamic. Their legacy counterparts panic immediately on invalid
programmer input instead of failing later in a background goroutine.
All outbound writes are serialized through a bounded queue. Call and
Notify return when their contexts end; with a legacy rpc.Transport, the
underlying write may remain blocked until Close. Implement
rpc.ContextTransport when writes themselves must observe cancellation.
App-server request handling defaults to four workers and a queue of 64; tune
rpc.ClientOptions.ServerRequestWorkers and
ServerRequestQueueCapacity when using the low-level client.
Inbound JSON-RPC messages are limited to 8 MiB by default. Set
rpc.ClientOptions.MaxMessageBytes to apply a smaller client-side acceptance
limit. Built-in line transports retain the 8 MiB allocation ceiling even if
the client option is larger. Custom transports return an already allocated
string, so they must enforce their own read/allocation limit; the client option
then rejects oversized returned values. Oversized messages close the client
with an error matching rpc.ErrMessageTooLarge.