Idiomatic Go SDK for the OpenAI Codex JSON-RPC 2.0 protocol, app-server runtime helpers, and Codex login helpers. Stdlib only, zero external dependencies.
Built against the Codex app-server protocol schemas. Schema-derived coverage tests keep the typed request, notification, and server-request surfaces aligned with the checked-in protocol.
go get github.com/dominicnunez/codex-sdk-goImport the package that matches the layer you need:
import codex "github.com/dominicnunez/codex-sdk-go/appserver/protocol"- Go 1.27.3+
- A Codex CLI binary for
appserverprocess helpers - An absolute
ProcessOptions.BinaryPathwhen spawning Codex from this SDK
Build and test with the default Go JSON backend; no experiment setting is
required. CI reads the Go version from go.mod.
| Package | Purpose | Upstream owner |
|---|---|---|
appserver/protocol |
Typed JSON-RPC requests, responses, notifications, approval handlers, and schema coverage | codex-rs/app-server-protocol |
appserver/transport |
Newline-delimited JSON-RPC stdio transport and notification ordering | codex-rs/app-server-transport/src/transport |
appserver |
codex app-server --listen stdio:// process startup, client lifecycle, single-turn Run, streamed turns, and persistent conversations |
codex-rs/app-server, codex-rs/app-server-client |
login |
Codex OAuth authorization-code flow with PKCE and local/manual callback handling | codex-rs/login |
login/auth |
Credential storage, JWT claim extraction, redaction, and chatgptAuthTokens payload helpers |
codex-rs/login, codex-rs/app-server-protocol |
- Protocol guide — service methods, inputs, notifications, approvals, and login variants
- Configuration guide — single and multiple workspace login restrictions and JSON composition
- Spec-sync maintenance — automated schema updates and superseded-PR handling
- Threat model — trust boundaries, implemented controls, residual risks, and severity calibration
For maintainers, see the architecture and ownership reference and protocol maintenance cases.
Use appserver as the default SDK runtime when you need typed protocol access,
persistent threads, streaming notifications, approvals, account/login methods, or
multi-turn helper flows. It starts codex app-server --listen stdio:// and keeps
that process alive for the client lifecycle.
Direct Codex CLI exec-mode JSONL support (codex exec --json) is intentionally
not exposed by this app-server-first API. If needed later, it should live behind
a separate exec package with JSONL event parsing rather than sharing the
app-server Process type.
The appserver/protocol package is protocol-only. It provides typed JSON-RPC requests,
notifications, responses, and approval handlers over a caller-provided codex.Transport.
func run(ctx context.Context, transport codex.Transport) error {
client := codex.NewClient(transport, codex.WithRequestTimeout(30*time.Second))
// Initialize handshake
_, err := client.Initialize(ctx, codex.InitializeParams{
ClientInfo: codex.ClientInfo{
Name: "my-codex-client",
Version: "1.0.0",
},
})
if err != nil {
return err
}
// Listen for protocol notifications
client.OnAgentMessageDelta(func(notif codex.AgentMessageDeltaNotification) {
fmt.Print(notif.Delta)
})
// Start a thread and turn
threadResp, err := client.Thread.Start(ctx, codex.ThreadStartParams{
Model: codex.Ptr("gpt-4"),
})
if err != nil {
return err
}
_, err = client.Turn.Start(ctx, codex.TurnStartParams{
ThreadID: threadResp.Thread.ID,
Input: []codex.UserInput{
&codex.TextUserInput{Text: "What is the capital of France?"},
},
})
return err
}The appserver package starts codex app-server --listen stdio://, initializes
the app-server lifecycle, and exposes ergonomic helpers for Run, RunStreamed,
and StartConversation.
import (
"context"
"fmt"
osexec "os/exec"
"github.com/dominicnunez/codex-sdk-go/appserver"
)
func runCodex(ctx context.Context) error {
binary, err := osexec.LookPath("codex")
if err != nil {
return err
}
server, err := appserver.StartProcess(ctx, &appserver.ProcessOptions{
BinaryPath: binary,
})
if err != nil {
return err
}
defer server.Close()
if _, err := server.Initialize(ctx); err != nil {
return err
}
result, err := server.Run(ctx, appserver.RunOptions{
Prompt: "Summarize this repository.",
})
if err != nil {
return err
}
fmt.Println(result.Response)
return nil
}Use RunStreamed for range-over-func streaming events, or StartConversation when you need a persistent thread across multiple turns.
Use login and login/auth to obtain ChatGPT/Codex subscription-backed tokens and pass them to the app-server as chatgptAuthTokens.
import (
"context"
"fmt"
"sync"
codex "github.com/dominicnunez/codex-sdk-go/appserver/protocol"
9DD4
span>
"github.com/dominicnunez/codex-sdk-go/login"
"github.com/dominicnunez/codex-sdk-go/login/auth"
)
func loginWithAuthTokens(ctx context.Context, client *codex.Client, credentialPath string) error {
creds, err := login.Login(ctx, login.LoginOptions{
Config: login.Config{Originator: "my-codex-client"},
OnAuthURL: func(ctx context.Context, authURL string) error {
fmt.Println("Open this URL:", authURL)
return nil
},
ManualCode: func(ctx context.Context, prompt login.AuthPrompt) (string, error) {
fmt.Println(prompt.Message)
var input string
_, err := fmt.Scanln(&input)
return input, err
},
})
if err != nil {
return err
}
if err := auth.SaveCredentials(credentialPath, creds); err != nil {
return err
}
var credsMu sync.Mutex
client.SetApprovalHandlers(codex.ApprovalHandlers{
OnChatgptAuthTokensRefresh: func(ctx context.Context, params codex.ChatgptAuthTokensRefreshParams) (codex.ChatgptAuthTokensRefreshResponse, error) {
credsMu.Lock()
defer credsMu.Unlock()
refreshed, err := login.Refresh(ctx, login.Config{Originator: "my-codex-client"}, creds.RefreshToken)
if err != nil {
return codex.ChatgptAuthTokensRefreshResponse{}, err
}
if err := auth.SaveCredentials(credentialPath, refreshed); err != nil {
return codex.ChatgptAuthTokensRefreshResponse{}, err
}
creds = refreshed
payload, err := auth.NewAuthTokensRefreshResponse(refreshed)
if err != nil {
return codex.ChatgptAuthTokensRefreshResponse{}, err
}
return codex.ChatgptAuthTokensRefreshResponse{
AccessToken: payload.AccessToken,
ChatgptAccountID: payload.ChatGPTAccountID,
ChatgptPlanType: payload.ChatGPTPlanType,
}, nil
},
})
credsMu.Lock()
payload, err := auth.NewAuthTokensLoginParams(creds)
credsMu.Unlock()
if err != nil {
return err
}
_, err = client.Account.Login(ctx, &codex.ChatgptAuthTokensLoginAccountParams{
Type: payload.Type,
AccessToken: payload.AccessToken,
ChatgptAccountId: payload.ChatGPTAccountID,
ChatgptPlanType: payload.ChatGPTPlanType,
})
return err
}This is a Codex app-server auth bridge. It is not OpenAI Platform API-key auth and it does not make ChatGPT subscriptions usable with generic api.openai.com API endpoints.
Codex is bidirectional — the server sends requests back to the client for approval. Register handlers to respond:
client.SetApprovalHandlers(codex.ApprovalHandlers{
OnFileChangeRequestApproval: func(ctx context.Context, params codex.FileChangeRequestApprovalParams) (codex.FileChangeRequestApprovalResponse, error) {
return codex.FileChangeRequestApprovalResponse{Decision: "accept"}, nil
},
OnCommandExecutionRequestApproval: func(ctx context.Context, params codex.CommandExecutionRequestApprovalParams) (codex.CommandExecutionRequestApprovalResponse, error) {
return codex.CommandExecutionRequestApprovalResponse{
Decision: codex.CommandExecutionApprovalDecisionWrapper{Value: "accept"},
}, nil
},
})Unhandled approval types return JSON-RPC method-not-found (-32601).
See the protocol guide for structured denial decisions and other approval-specific details.
JSON-RPC 2.0 over a pluggable transport layer. The protocol is bidirectional:
- Client → Server: Requests and notifications
- Server → Client: Approval requests and streaming notifications
Services: client.Thread, client.Turn, client.Account, client.Config, client.Model, client.Skills, client.Apps, client.Mcp, client.Command, client.Review, client.Feedback, client.ExternalAgent, client.Experimental, client.System
Runtime helpers import the protocol package instead of redefining schema-owned
types, so appserver/protocol/schema/json/ remains the source of truth for the app-server contract.
Process helpers reject relative binary paths. Pass credentials through supported environment variables or login/auth payloads instead of command-line arguments.
Built from the JSON schemas in the OpenAI Codex app-server protocol and selected upstream login/runtime behavior. Local package structure follows the upstream codex-rs/ owner paths where practical while preserving Go package boundaries. This is an unofficial community SDK.
Issues and PRs welcome on GitHub.
This repo uses shared Git hooks in .githooks/.
Install hooks once after cloning:
git config --local core.hooksPath .githooksVerify the setting:
git config --local --get core.hooksPathHook behavior:
pre-commit: checks formatting and lint against a private copy of the staged snapshot. It leaves working files and staging unchanged; files needing formatting must be formatted and staged before committing.pre-push: runsgo test ./...,go test -race ./...,golangci-lint run ./..., andgo mod tidy -diffagainst a private copy of clean, checked-outHEAD. Other revisions and tracked edits are rejected before checks; working files and staging remain unchanged.
The hook runner reuses only golangci-lint v2.14.0, matching CI, and installs that version with Go when needed. Hooks preserve caller-supplied Go experiments. Nix is optional; without it, hooks use the current environment's Go toolchain, which must meet the Go 1.27.3 minimum.
Hook verification describes staged-file selection, lint scope, supported file identities and verification cases.
To bypass hooks intentionally for a one-off operation, use Git's standard --no-verify flag.