Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ndycode/codex-multi-auth/llms.txt
Use this file to discover all available pages before exploring further.
Plugin Architecture
Codex Multi-Auth is built as a Codex CLI plugin using the @codex-ai/plugin framework. This architecture enables seamless integration with the Codex runtime while providing multi-account OAuth management and request routing.
Plugin Structure
The plugin exports a standard Plugin interface from @codex-ai/plugin with three main components:
Core Components
import { tool } from "@codex-ai/plugin/tool";
import type { Plugin, PluginInput } from "@codex-ai/plugin";
export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
return {
event: eventHandler, // Handle runtime events
auth: { ... }, // OAuth authentication provider
tools: { ... }, // Registered tool handlers
};
};
Authentication Provider
The plugin registers as an OAuth provider with the Codex runtime:
auth: {
provider: "openai",
async loader(getAuth, provider) {
// 1. Load multi-account pool from disk
// 2. Apply per-project/global storage scope
// 3. Return SDK configuration with custom fetch
return {
apiKey: "codex-dummy-key",
baseURL: "https://chatgpt.com",
fetch: customFetchImplementation
};
}
}
Custom Fetch Pipeline
The plugin intercepts all OpenAI SDK calls with a 7-step request pipeline:
- URL Rewriting:
/v1/chat/completions → https://chatgpt.com/backend-api/conversation
- Token Refresh: Check expiry, refresh if needed (with skew window)
- Request Transform: Inject model-specific Codex instructions, apply fast-session defaults
- Account Selection: Choose account based on health, rate limits, session affinity
- Header Injection: Add OAuth bearer token, Codex-specific headers
- Stream Handling: Convert SSE to JSON, handle failover on stream errors
- Error Recovery: Retry with backoff, rotate accounts on rate limits
The plugin registers tools using the @codex-ai/plugin/tool API:
import { tool } from "@codex-ai/plugin/tool";
const hashlineReadTool = tool({
description: "Read file lines with hashline refs (L<line>#<hash>)",
args: {
path: tool.schema.string().describe("File path"),
startLine: tool.schema.number().optional(),
maxLines: tool.schema.number().optional()
},
async execute({ path, startLine, maxLines }, context) {
const absolutePath = resolveToolPath(path, context);
await context.ask({ permission: "read", patterns: [absolutePath] });
const content = await readFile(absolutePath, "utf8");
return renderHashlineSlice(content, startLine, maxLines);
}
});
Tool Context
Tools receive a ToolContext with session metadata:
interface ToolContext {
directory: string; // Session working directory
worktree?: string; // Git worktree root (if available)
ask(options): Promise<void>; // Permission request API
}
The plugin provides two advanced file editing tools:
hashline_read
Reads files with SHA-1 hash verification for each line:
{
path: "src/index.ts",
startLine: 100,
maxLines: 50
}
// Returns:
// L100#a3f2c8d1 | export const foo = () => {
// L101#b9e4f7a2 | return "bar";
// L102#c5d1a8f3 | };
hashline_edit
Performs hash-verified line edits with deterministic operation modes:
Replace Operation:
{
path: "src/index.ts",
lineRef: "L100#a3f2c8d1",
operation: "replace",
content: "export const foo = (value: string) => {\n return value;\n}"
}
Insert Operations:
// Insert before line
{
lineRef: "L50#abc123",
operation: "insert_before",
content: "// New comment\n"
}
// Insert after line
{
lineRef: "L50#abc123",
operation: "insert_after",
content: "console.log('debug');\n"
}
Delete Operation:
// Delete single line
{
lineRef: "L50#abc123",
operation: "delete"
}
// Delete range
{
lineRef: "L50#abc123",
endLineRef: "L55#def456",
operation: "delete"
}
Legacy Mode Fallback:
// Falls back to exact string match when lineRef is absent
{
path: "src/index.ts",
oldString: "const foo = 'bar';",
newString: "const foo = 'baz';",
replaceAll: false // Fails if multiple matches found
}
Event Handler
The plugin listens for runtime events to synchronize account selection:
event: async ({ event }) => {
if (event.type === "account.select") {
const { index } = event.properties;
// Update active account in storage
storage.activeIndex = index;
await saveAccounts(storage);
// Sync to Codex CLI if enabled
await syncCodexCliActiveSelection(index);
}
}
Supported events:
account.select - User switches account via TUI hotkey (1-9)
openai.account.select - Provider-specific account selection
Plugin Lifecycle
Initialization (loader)
- Load plugin configuration from environment/Codex.json
- Apply UI theme and runtime settings
- Set storage scope (per-project vs global)
- Enable session affinity, refresh guardian, preemptive quota
- Load account pool with Codex CLI sync reconciliation
- Start live account sync watcher (if enabled)
- Prewarm Codex instructions and host prompts
Request Handling (fetch)
- Sync cached account manager from disk if stale
- Extract and rewrite request URL
- Parse request body, capture original stream flag
- Transform request with Codex instructions
- Resolve session affinity key from
CODEX_THREAD_ID or prompt_cache_key
- Select account (prefer session-pinned, fallback to health-scored rotation)
- Execute request with retry/failover logic
- Update account health, rate limits, quota metadata
- Save updated storage on rotation or token refresh
Shutdown
Cleanup handlers registered via registerCleanup():
- Stop live account sync watcher
- Stop refresh guardian interval
- Flush pending storage writes
When creating custom tools for the plugin:
Permission Requests
Always request permissions before file access:
await context.ask({
permission: "read",
patterns: [absolutePath],
always: [absolutePath], // Skip prompt for this path in future
metadata: { path: absolutePath }
});
Path Resolution
Resolve paths relative to session directory:
function resolveToolPath(pathValue: string, context: ToolContext): string {
const trimmed = pathValue.trim();
const baseDir = context.directory || process.cwd();
return normalize(isAbsolute(trimmed) ? trimmed : resolve(baseDir, trimmed));
}
Show relative paths when possible:
function toDisplayPath(absolutePath: string, context: ToolContext): string {
const root = context.worktree || context.directory || process.cwd();
const rel = relative(root, absolutePath);
if (!rel || rel.startsWith("..")) {
return absolutePath;
}
return rel.replace(/\\/g, "/"); // Unix-style separators
}
Error Handling
Return user-friendly error messages:
try {
const content = await readFile(absolutePath, "utf8");
return processContent(content);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Failed to read ${displayPath}: ${message}`;
}
Plugin Configuration
Configure the plugin in your Codex.json:
{
"plugin": ["codex-multi-auth"],
"model": "openai/gpt-5-codex",
"provider": {
"openai": {
"options": {
"CODEX_MODE": true,
"CODEX_FAST_SESSION": false,
"CODEX_SESSION_RECOVERY": true,
"CODEX_AUTH_PER_PROJECT_ACCOUNTS": false
}
}
}
}
See Configuration Reference for all available options.
Next Steps