@pulsemcp/air-adapter-codex
v0.13.1
Published
AIR adapter for OpenAI Codex CLI — translates AIR config to Codex format
Readme
@pulsemcp/air-adapter-codex
AIR adapter extension for the OpenAI Codex CLI. Translates AIR artifacts into Codex's native formats and prepares working directories for agent sessions.
Installation
npm install @pulsemcp/air-adapter-codexUsage
With the AIR CLI
# Install the adapter globally alongside the CLI
npm install -g @pulsemcp/air-cli @pulsemcp/air-adapter-codex
# Start a Codex session
air start codex --root web-appProgrammatic
import { resolveArtifacts } from "@pulsemcp/air-core";
import { CodexAdapter } from "@pulsemcp/air-adapter-codex";
const artifacts = await resolveArtifacts("./air.json");
const adapter = new CodexAdapter();
// Prepare a working directory for a Codex session
const session = await adapter.prepareSession(artifacts, "./my-project", {
root: artifacts.roots["web-app"],
});
// session.configFiles — [] (Codex config is TOML, see "Secrets" below)
// session.skillPaths — skill dirs created in .agents/skills/
// session.hookPaths — hook dirs created in .codex/hooks/
// session.startCommand — { command: "codex", args: [], cwd: "..." }What prepareSession() does
- Writes
.codex/config.toml— translates AIR MCP server configs into[mcp_servers.*]tables and registers path-based hooks under[[hooks.<Event>]]. User-authored servers, hooks, and top-level keys are preserved; only AIR-owned keys are replaced. - Injects skills — copies
SKILL.mdfiles and associated content into.agents/skills/{name}/, where Codex discovers them. - Injects hooks — copies hook directories into
.codex/hooks/{name}/and registers their command inconfig.toml, anchored to the repo root. - Copies references — attaches referenced documents into
{artifact}/references/. - Respects local priority — if a skill or hook directory already exists in the target, it is not overwritten.
Translation Details
| AIR Format | Codex Format |
|------------|--------------|
| mcp.json (flat map with type, title, description) | [mcp_servers.<name>] tables in .codex/config.toml (metadata stripped) |
| stdio servers | { command, args, env, env_vars } |
| sse / streamable-http servers | { url, http_headers, env_http_headers, bearer_token_env_var, oauth } (Codex auto-detects transport from the URL) |
| MCP env value ${VAR} where key == VAR | env_vars = ["VAR"] (host env forwarding) |
| MCP env value ${OTHER} (renamed) or "Bearer ${TOKEN}" (partial) | sh -c rebind shim (command = "sh", source forwarded via env_vars) |
| MCP env literal value | left in the env table |
| Header value ${VAR} (whole-value, renamed or not) | env_http_headers = { Header = "VAR" } |
| Header Authorization: "Bearer ${VAR}" | bearer_token_env_var = "VAR" |
| Non-Bearer partial header ("v1-${TOKEN}") | left literal in http_headers + console.warn |
| MCP oauth.clientId | per-server [mcp_servers.<name>.oauth] client_id |
| MCP oauth.redirectUri | single top-level mcp_oauth_callback_url (global) |
| Skills (SKILL.md + content) | .agents/skills/{name}/ |
| Hooks (HOOK.json + scripts) | .codex/hooks/{name}/ + [[hooks.<Event>]] registration |
| Hook events session_start, pre_tool_call, post_tool_call, user_prompt_submit, stop | Codex SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, Stop |
| References | {artifact}/references/ |
Secrets
Codex's config is TOML, which is outside AIR's JSON-based transform/validation pipeline. Instead of writing ${VAR} placeholders, the adapter maps secret references to Codex-native mechanisms at translation time so only variable names — never values — land in config.toml. As a result, prepareSession() returns an empty configFiles array — there is no JSON config for secret transforms to post-process.
stdio env:
env: { GITHUB_TOKEN: "${GITHUB_TOKEN}" }(whole-value, same name) →env_vars = ["GITHUB_TOKEN"]— Codex injects the host'sGITHUB_TOKENat launch.env: { TOKEN: "${GITHUB_TOKEN}" }(renamed) orenv: { AUTH: "Bearer ${TOKEN}" }(partial) → Codex'senv_varscan express neither, so the launch is wrapped in ash -cshim that rebinds the key from the forwarded source var right beforeexec:command = "sh",args = ["-c", "AUTH=\"Bearer ${TOKEN}\" exec <orig command/args>"], with the source forwarded viaenv_vars = ["TOKEN"]. The secret value never touches disk — only the variable name does.- A literal value (no
${…}) stays in the[mcp_servers.<name>.env]table.
remote-server headers:
headers: { Authorization: "${API_TOKEN}" }(whole-value, renamed or not) →env_http_headers = { Authorization = "API_TOKEN" }.headers: { Authorization: "Bearer ${API_TOKEN}" }→bearer_token_env_var = "API_TOKEN"(Codex emits theAuthorizationheader itself).- A non-Bearer partial header (
X-Api-Key = "v1-${TOKEN}") has no Codex expression — remote servers have no launch process to wrap in a shell shim — so it stays literal inhttp_headersand the adapter emits aconsole.warn. Rewrite these as a whole-value ref (or set the value directly).
OAuth (remote servers):
oauth.clientId→ per-server[mcp_servers.<name>.oauth]client_id. Emitting an explicitclient_idbypasses OAuth dynamic client registration (RFC 7591), which some providers reject.oauth.redirectUri→ the single top-levelmcp_oauth_callback_url. Codex has no per-server redirect URI, so if multiple servers declare distinct URIs the adapter keeps the first and warns.oauth.scopes,oauth.clientSecret, andoauth.authServerMetadataUrlhave no Codex per-server config slot (theoauthtable accepts onlyclient_id), so they are dropped with a warning rather than silently.
Known gaps
These AIR features have no static Codex equivalent and are handled out of band:
- Plugins — Codex's marketplace plugins are remote-installed (
codex plugin add). AIR treats plugins as composition sugar: a plugin's declared MCP servers / skills / hooks are expanded into the activation set and materialized as their underlying Codex-native artifacts. - Subagent context — Codex has no
--append-system-promptflag, so subagent-root context is returned to the caller viaPreparedSession.subagentContextrather than passed to the CLI.
