Skip to content

Instantly share code, notes, and snippets.

@clairernovotny
Last active April 17, 2026 16:06
Show Gist options
  • Select an option

  • Save clairernovotny/89587e4932d854b10bbab913b95ecb5c to your computer and use it in GitHub Desktop.

Select an option

Save clairernovotny/89587e4932d854b10bbab913b95ecb5c to your computer and use it in GitHub Desktop.
Codex plugins and marketplaces developer notes from source analysis

Codex Plugins and Marketplaces: Developer Notes

Scanned source: openai/codex main at commit dae0608c06bf61a356209fd11243aec1ef816547 on April 17, 2026.

Status

The plugin feature is present in source and the feature flag is stable/default-enabled.

Mental Model

A plugin is a local bundle of optional capabilities:

  • skills
  • MCP servers
  • ChatGPT app connector IDs
  • manifest metadata used by marketplace and app-server UI surfaces

An installed plugin is copied into $CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version> and enabled through user config. Loading a plugin means reading the enabled user config entry, resolving the active cache directory, loading skills/MCP/apps from that directory, and exposing those capabilities to the session.

A marketplace is a catalog root with a supported manifest at:

  • <root>/.agents/plugins/marketplace.json
  • <root>/.claude-plugin/marketplace.json

The marketplace manifest contributes:

  • a marketplace name and display name
  • ordered plugin entries
  • local plugin source paths
  • install and auth policy
  • product restrictions
  • category metadata

The official curated marketplace is named openai-curated. Codex mirrors it under $CODEX_HOME/.tmp/plugins and records its synced revision in $CODEX_HOME/.tmp/plugins.sha.

Crate Layout

The implementation is split across several crates/modules:

User Config

Plugin state and added marketplace state are user-config state. Plugin entries are read from the user layer in core-plugins/src/loader.rs. Configured marketplaces are read from the user layer in core/src/plugins/installed_marketplaces.rs.

Plugin config shape:

[plugins."my-plugin@debug"]
enabled = true

Marketplace config shape written by marketplace/add:

[marketplaces.debug]
last_updated = "2026-04-17T12:00:00Z"
source_type = "git"
source = "https://github.com/example/codex-marketplace.git"
ref = "main"
sparse_paths = [".agents", "plugins"]

For a local marketplace:

[marketplaces.debug]
last_updated = "2026-04-17T12:00:00Z"
source_type = "local"
source = "C:\\Users\\me\\codex-marketplace"

Important details:

Plugin Cache

Installed plugin path:

$CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version>

Version selection:

  • plugin.json version is used when present.
  • Missing version uses local.
  • openai-curated installs use the current curated marketplace revision from $CODEX_HOME/.tmp/plugins.sha.
  • Valid version path segments allow ASCII letters, digits, ., +, _, and -.

Install behavior from core-plugins/src/store.rs:

  • validates source directory existence
  • requires a loadable plugin manifest
  • requires the manifest/plugin-root name to match the marketplace plugin name
  • copies the full plugin directory into a staged cache directory
  • atomically replaces the plugin cache root
  • records the installed version and installed path in the install result

Uninstall behavior:

  • removes $CODEX_HOME/plugins/cache/<marketplace>/<plugin>
  • succeeds if the target is already absent

Plugin Bundle Contract

Default plugin layout:

my-plugin/
  .codex-plugin/
    plugin.json
  skills/
    my-skill/
      SKILL.md
  .mcp.json
  .app.json

Supported manifest locations:

my-plugin/.codex-plugin/plugin.json
my-plugin/.claude-plugin/plugin.json

Minimal manifest:

{
  "name": "my-plugin"
}

Manifest with supported fields:

{
  "name": "my-plugin",
  "version": "1.2.3",
  "description": "Plugin summary for model-facing capability listings.",
  "skills": "./plugin-skills",
  "mcpServers": "./plugin.mcp.json",
  "apps": "./plugin.apps.json",
  "interface": {
    "displayName": "My Plugin",
    "shortDescription": "Shown in compact listings.",
    "longDescription": "Shown in richer details.",
    "developerName": "Example, Inc.",
    "category": "Productivity",
    "capabilities": ["Interactive", "Write"],
    "websiteURL": "https://example.com/",
    "privacyPolicyURL": "https://example.com/privacy",
    "termsOfServiceURL": "https://example.com/terms",
    "defaultPrompt": [
      "Summarize my inbox",
      "Find my next action"
    ],
    "brandColor": "#3B82F6",
    "composerIcon": "./assets/icon.png",
    "logo": "./assets/logo.png",
    "screenshots": [
      "./assets/shot1.png",
      "./assets/shot2.png"
    ]
  }
}

Parsed top-level fields from core-plugins/src/manifest.rs:

  • name
  • version
  • description
  • skills
  • mcpServers
  • apps
  • interface

Path rules:

  • custom paths must start with ./
  • custom paths cannot be exactly ./
  • custom paths cannot contain ..
  • custom paths resolve under the plugin root
  • invalid custom paths are ignored with warnings

interface.defaultPrompt rules:

  • accepts a legacy string or an array
  • normalizes whitespace
  • returns an array in app-server protocol responses
  • keeps at most 3 prompts
  • ignores empty prompts
  • ignores prompts longer than 128 characters
  • ignores non-string array entries

Interface asset fields (composerIcon, logo, screenshots) use the same custom path resolver as component paths.

Skills in Plugins

Plugin skill roots from core-plugins/src/loader.rs:

  • default skills/ is included when present
  • manifest skills is also included when present
  • duplicate roots are sorted and deduped
  • plugin skill roots use SkillScope::User

Skill names are namespaced from the nearest plugin manifest via utils/plugins/src/plugin_namespace.rs and core-skills/src/loader.rs:

<plugin-name>:<skill-name>

Product filtering:

  • plugin skill metadata can carry product restrictions
  • plugin/read filters returned skills for the session product
  • plugin loading also filters plugin skills by the manager’s product restriction

Enabled state:

  • skill enablement/disablement is resolved through normal skills config rules
  • plugin/read returns each skill’s effective enabled state

MCP Servers in Plugins

Plugin MCP config is loaded from core-plugins/src/loader.rs:

  • manifest mcpServers replaces default .mcp.json discovery
  • without manifest mcpServers, Codex uses .mcp.json when present
  • the file shape is { "mcpServers": { "<name>": { ... } } }
  • relative cwd values are rebased to the plugin root
  • oauth.callbackPort in plugin MCP config is ignored because Codex uses global MCP OAuth callback settings
  • transport type accepts http, streamable_http, streamable-http, and stdio; unknown values warn

Effective MCP merging:

  • configured user/project MCP servers are loaded first
  • plugin MCP servers are added with entry(...).or_insert(...), so an existing configured MCP server with the same name wins
  • duplicate MCP server names across plugins are warned about; the first effective plugin server wins

Install-time behavior:

  • plugin/install loads plugin MCP servers from the installed cache
  • if any exist, app-server queues an MCP server refresh
  • app-server starts silent OAuth login attempts for plugin MCP servers that advertise supported MCP OAuth
  • OAuth completion emits mcpServer/oauthLogin/completed

Relevant sources: core-plugins/src/loader.rs, core/src/config/mod.rs, app-server plugin_mcp_oauth.rs.

Apps in Plugins

Plugin app config is loaded from core-plugins/src/loader.rs:

  • manifest apps replaces default .app.json discovery
  • without manifest apps, Codex uses .app.json when present
  • the file shape is { "apps": { "<entry>": { "id": "<connector-id>" } } }
  • blank connector IDs are ignored
  • duplicate connector IDs are deduped

Install/read behavior:

  • plugin/read returns app summaries and includes needsAuth when connector accessibility can be determined
  • plugin/install returns appsNeedingAuth for plugin apps that are in app metadata but not accessible
  • app metadata/provenance flows through connector metadata and MCP tool provenance

Relevant sources: core-plugins/src/loader.rs, app-server plugin_app_helpers.rs, core/src/connectors.rs.

Marketplace Contract

Marketplace manifest locations:

<root>/.agents/plugins/marketplace.json
<root>/.claude-plugin/marketplace.json

Marketplace example:

{
  "name": "debug",
  "interface": {
    "displayName": "Debug Marketplace"
  },
  "plugins": [
    {
      "name": "my-plugin",
      "source": {
        "source": "local",
        "path": "./plugins/my-plugin"
      },
      "policy": {
        "installation": "AVAILABLE",
        "authentication": "ON_USE",
        "products": ["CODEX", "CHATGPT"]
      },
      "category": "Productivity"
    }
  ]
}

Supported plugin source forms:

"source": "./plugins/my-plugin"
"source": {
  "source": "local",
  "path": "./plugins/my-plugin"
}

Source path rules from core-plugins/src/marketplace.rs:

  • only local plugin sources are currently resolved by marketplace manifests
  • local paths must start with ./
  • local paths resolve relative to marketplace root, not relative to .agents/plugins/
  • local paths must stay inside the marketplace root
  • unsupported source shapes are skipped for that plugin entry

Policy defaults:

  • policy.installation defaults to AVAILABLE
  • policy.authentication defaults to ON_INSTALL
  • omitted policy.products means all products
  • empty policy.products means no product

Policy values:

  • policy.installation: NOT_AVAILABLE, AVAILABLE, INSTALLED_BY_DEFAULT
  • policy.authentication: ON_INSTALL, ON_USE
  • policy.products: protocol Product values such as CODEX and CHATGPT

Marketplace/interface behavior:

  • top-level interface.displayName is surfaced on marketplace entries
  • plugin category overrides plugin.json interface.category
  • invalid marketplace files are returned as marketplaceLoadErrors instead of aborting the whole list
  • invalid plugin entries inside a marketplace are skipped with warnings

Marketplace Discovery

plugin/list is the primary discovery surface.

Discovery rules:

  • home-scoped marketplace discovery is attempted first
  • caller-provided cwds are additional roots
  • configured marketplace roots from [marketplaces] are additional roots
  • the curated mirror root $CODEX_HOME/.tmp/plugins is appended when it exists
  • each additional root is checked directly for a marketplace manifest
  • if a root is inside a Git repository, Codex also checks the Git repo root for a marketplace manifest
  • duplicate marketplace paths are deduped
  • duplicate plugin keys are deduped across marketplaces and the first discovered plugin key wins

Configured marketplace roots:

  • Git marketplaces added with marketplace/add install under $CODEX_HOME/.tmp/marketplaces/<marketplace>
  • local marketplaces added with marketplace/add point at their original local directory
  • configured roots are resolved from the user [marketplaces] table

Relevant sources: core-plugins/src/marketplace.rs, core/src/plugins/manager.rs, core/src/plugins/installed_marketplaces.rs.

Product Restrictions

Marketplace entries can restrict plugin visibility with policy.products.

{
  "name": "chatgpt-only",
  "source": "./plugins/chatgpt-only",
  "policy": {
    "installation": "AVAILABLE",
    "authentication": "ON_INSTALL",
    "products": ["CHATGPT"]
  }
}

Enforcement:

  • plugin/list filters plugins against the PluginsManager restriction product
  • plugin/read returns not found for product-disallowed plugins
  • plugin/install rejects product-disallowed plugins as not available
  • remote curated sync skips local curated plugins disallowed by product
  • plugin skill loading filters skills against the same restriction product

The default PluginsManager::new() restriction product is CODEX.

Design note from source: product restrictions are admission-time gating for a CODEX_HOME; runtime plugin loading trusts already-admitted local config/cache and assumes a CODEX_HOME is used by one product.

marketplace/add

marketplace/add adds a marketplace source to user config and returns the resolved root.

Protocol shape:

{
  "method": "marketplace/add",
  "id": 1,
  "params": {
    "source": "owner/repo@main",
    "refName": null,
    "sparsePaths": [".agents", "plugins"]
  }
}

Response shape:

{
  "marketplaceName": "debug",
  "installedRoot": "/abs/path/to/marketplace/root",
  "alreadyAdded": false
}

Accepted source forms from core/src/plugins/marketplace_add/source.rs:

  • local directory path: absolute, ./..., ../..., ~/..., Windows drive path, or UNC path
  • HTTPS Git URL
  • HTTP Git URL
  • SSH Git URL such as ssh://... or git@host:owner/repo.git
  • GitHub shorthand owner/repo

Ref handling:

  • Git URLs can use #ref
  • GitHub shorthand can use owner/repo@ref
  • explicit refName overrides a source suffix
  • refs are rejected for local directory sources

Sparse checkout:

  • sparsePaths is allowed only for Git sources
  • Git sources with sparse paths are cloned with --filter=blob:none --no-checkout, then git sparse-checkout set ..., then checkout

Install behavior:

  • local directory sources are not copied; Codex records their canonical path in user config
  • Git sources are cloned into $CODEX_HOME/.tmp/marketplaces/<safe-marketplace-name>
  • marketplace name openai-curated is reserved and cannot be added
  • adding the same source again returns alreadyAdded: true
  • adding the same marketplace name from a different source is rejected

Relevant sources: marketplace_add.rs, marketplace_add/source.rs, marketplace_add/install.rs, marketplace_add/metadata.rs.

plugin/list

Protocol shape:

{
  "method": "plugin/list",
  "id": 2,
  "params": {
    "cwds": ["/absolute/workspace/path"],
    "forceRemoteSync": false
  }
}

cwds is optional. If omitted, listing includes home-scoped marketplaces, configured marketplaces, and the curated marketplace when available.

Response shape:

{
  "marketplaces": [
    {
      "name": "debug",
      "path": "/abs/root/.agents/plugins/marketplace.json",
      "interface": {
        "displayName": "Debug Marketplace"
      },
      "plugins": [
        {
          "id": "my-plugin@debug",
          "name": "my-plugin",
          "source": {
            "type": "local",
            "path": "/abs/root/plugins/my-plugin"
          },
          "installed": false,
          "enabled": false,
          "installPolicy": "AVAILABLE",
          "authPolicy": "ON_INSTALL",
          "interface": {
            "displayName": "My Plugin",
            "defaultPrompt": ["Summarize my inbox"]
          }
        }
      ]
    }
  ],
  "marketplaceLoadErrors": [],
  "remoteSyncError": null,
  "featuredPluginIds": ["linear@openai-curated"]
}

Behavior:

  • schedules a background non-curated cache refresh for configured plugins under the requested cwds
  • optionally reconciles curated plugin local state with ChatGPT remote state when forceRemoteSync is true
  • returns local marketplace data even if remote sync fails, with remoteSyncError
  • includes marketplace load failures in marketplaceLoadErrors
  • fetches featuredPluginIds only when the openai-curated marketplace is present
  • featured plugin fetch is best-effort and returns an empty list on failure

Relevant sources: codex_message_processor.rs, manager.rs, protocol/v2.rs.

plugin/read

Protocol shape:

{
  "method": "plugin/read",
  "id": 3,
  "params": {
    "marketplacePath": "/abs/root/.agents/plugins/marketplace.json",
    "pluginName": "my-plugin"
  }
}

Response shape:

{
  "plugin": {
    "marketplaceName": "debug",
    "marketplacePath": "/abs/root/.agents/plugins/marketplace.json",
    "summary": {
      "id": "my-plugin@debug",
      "name": "my-plugin",
      "source": {
        "type": "local",
        "path": "/abs/root/plugins/my-plugin"
      },
      "installed": true,
      "enabled": true,
      "installPolicy": "AVAILABLE",
      "authPolicy": "ON_USE",
      "interface": {
        "displayName": "My Plugin"
      }
    },
    "description": "Plugin summary for model-facing capability listings.",
    "skills": [],
    "apps": [],
    "mcpServers": []
  }
}

Behavior:

  • resolves the plugin from the exact marketplace path and plugin name
  • applies product gating
  • loads manifest description/interface
  • loads skills with product filtering and effective enabled state
  • loads app summaries and needsAuth where available
  • returns sorted/deduped MCP server names

Relevant sources: codex_message_processor.rs, manager.rs, plugin_app_helpers.rs.

plugin/install

Protocol shape:

{
  "method": "plugin/install",
  "id": 4,
  "params": {
    "marketplacePath": "/abs/root/.agents/plugins/marketplace.json",
    "pluginName": "my-plugin",
    "forceRemoteSync": false
  }
}

Response shape:

{
  "authPolicy": "ON_USE",
  "appsNeedingAuth": [
    {
      "id": "google-calendar",
      "name": "Google Calendar",
      "description": "Plan events and schedules.",
      "installUrl": "https://chatgpt.com/apps/google-calendar/calendar",
      "needsAuth": true
    }
  ]
}

Behavior:

  • rejects NOT_AVAILABLE marketplace entries
  • rejects product-disallowed marketplace entries
  • copies the plugin into $CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version>
  • writes [plugins."<plugin>@<marketplace>"].enabled = true to user config
  • clears plugin and skill caches
  • refreshes MCP server state when the plugin has MCP servers
  • starts silent MCP OAuth login attempts for supported plugin MCP servers
  • returns the effective plugin auth policy
  • computes appsNeedingAuth for inaccessible plugin app connectors when app support is available
  • when forceRemoteSync is true, calls the remote enable mutation before local install

Relevant sources: manager.rs, store.rs, codex_message_processor.rs.

plugin/uninstall

Protocol shape:

{
  "method": "plugin/uninstall",
  "id": 5,
  "params": {
    "pluginId": "my-plugin@debug",
    "forceRemoteSync": false
  }
}

Response shape:

{}

Behavior:

  • parses pluginId as <plugin>@<marketplace>
  • removes the plugin cache root
  • clears the plugin user config entry
  • clears plugin and skill caches
  • emits plugin uninstall telemetry when installed plugin metadata is available
  • when forceRemoteSync is true, calls the remote uninstall mutation before local uninstall

Relevant sources: manager.rs, store.rs, codex_message_processor.rs.

Curated Marketplace Sync

Local curated mirror paths:

$CODEX_HOME/.tmp/plugins
$CODEX_HOME/.tmp/plugins.sha

Startup mirror sync from core/src/plugins/startup_sync.rs:

  • first tries git ls-remote and a depth-1 clone of https://github.com/openai/plugins.git
  • falls back to GitHub API repository/ref lookup and zipball download
  • falls back to https://chatgpt.com/backend-api/plugins/export/curated only when no local curated snapshot exists
  • writes the selected revision to $CODEX_HOME/.tmp/plugins.sha
  • requires the extracted root to contain .agents/plugins/marketplace.json
  • replaces the local mirror atomically
  • removes stale temp dirs older than 10 minutes

Startup remote state sync:

  • runs once per app-server startup when plugins are enabled
  • waits up to 10 seconds for a local curated snapshot
  • calls ChatGPT /plugins/list
  • uses additive-only mode
  • writes marker $CODEX_HOME/.tmp/app-server-remote-plugin-sync-v1 after success
  • retries on the next app-server start if it fails

plugin/list(forceRemoteSync: true):

  • calls ChatGPT /plugins/list
  • requires ChatGPT auth
  • rejects API-key auth
  • reconciles enabled remote curated plugins into local cache/config
  • removes local curated plugins absent from the remote enabled set
  • treats remote enabled = false as uninstall
  • pins remote-installs to the current $CODEX_HOME/.tmp/plugins.sha revision
  • fails open at app-server boundary by returning local list data plus remoteSyncError

Featured plugin IDs:

  • fetched from ChatGPT /plugins/featured
  • includes platform query from the product restriction, defaulting to Codex
  • can use ChatGPT auth headers when available
  • also works without ChatGPT auth
  • cached in memory by base URL, account, and product

Relevant sources: startup_sync.rs, remote.rs, manager.rs.

Configured Marketplace Updates

Git marketplaces added through marketplace/add participate in automatic update checks.

Update behavior from core-plugins/src/marketplace_upgrade.rs:

  • configured Git marketplaces are selected from user [marketplaces]
  • Codex resolves the remote revision with git ls-remote
  • if the local installed root and .codex-marketplace-install.json match the recorded revision/source/ref/sparse paths, no update is applied
  • if an update is needed, Codex clones into a staging directory
  • the staged marketplace root must validate and have the same marketplace name as the configured entry
  • Codex writes .codex-marketplace-install.json into the activated root
  • Codex updates user config last_revision and last_updated
  • activation is atomic with rollback on config-write failure
  • after upgraded roots activate, installed non-curated plugin caches are force-refreshed

Manual install/update flow for a plugin:

  1. Add or discover a marketplace.
  2. Call plugin/list.
  3. Pick a marketplace path and plugin name.
  4. Call plugin/install.
  5. Codex writes the plugin cache and enables the plugin in user config.

Updating a plugin from a marketplace:

  • bump or change the source plugin directory
  • set plugin.json version to the desired cache version
  • refresh/update the marketplace source if needed
  • call plugin/install again

For configured Git marketplaces, startup auto-upgrade plus non-curated cache refresh can update installed plugin caches when the marketplace revision and plugin version change.

Mention / Invocation Contract

Plugin mentions use structured mention paths:

plugin://<plugin-name>@<marketplace-name>

Example app-server input item:

{
  "type": "mention",
  "name": "sample",
  "path": "plugin://sample@debug"
}

Text can also contain a linked plugin mention:

Use [@sample](plugin://sample@debug)

Behavior:

  • plugin plaintext links use @, not $
  • explicit plugin mentions are matched only against loaded plugin capability summaries
  • explicit mentions inject developer guidance naming the plugin’s skill prefix, visible MCP servers, and visible apps
  • plugin capabilities still come from skills, MCP servers, and apps; there is no separate plugin execution primitive

Relevant sources: core/src/plugins/mentions.rs, core/src/plugins/injection.rs, core/src/plugins/render.rs, core-skills/src/injection.rs, protocol/src/user_input.rs.

Tool Suggest Integration

Plugin suggestions are surfaced through the tool suggestion system when both apps and plugins are enabled.

Behavior from core/src/plugins/discoverable.rs:

  • only openai-curated and openai-bundled marketplace plugins are considered
  • already installed plugins are omitted
  • built-in allowlist includes GitHub, Notion, Slack, Gmail, Google Calendar, Google Drive, Linear, Figma, and bundled Computer Use
  • config can add discoverable plugin IDs through tool_suggest.discoverables
  • configured discoverables use { type = "plugin", id = "<plugin>@<marketplace>" }
  • missing configured plugin IDs are ignored

Example:

[tool_suggest]
discoverables = [
  { type = "plugin", id = "sample@openai-curated" }
]

Relevant sources: discoverable.rs, config/src/types.rs, tools/handlers/tool_suggest.rs.

Authoring Checklist

  1. Pick a plugin id using only ASCII letters, digits, _, and -.
  2. Create .codex-plugin/plugin.json or .claude-plugin/plugin.json.
  3. Include name; include version when you want deterministic cache versioning.
  4. Put skills under skills/ or declare an additional skills path.
  5. Put MCP servers in .mcp.json or declare mcpServers.
  6. Put app connector IDs in .app.json or declare apps.
  7. Add interface metadata for marketplace UI.
  8. Add the plugin to a marketplace manifest.
  9. Use marketplace/add for local or Git marketplace distribution, or pass a repo root through plugin/list.cwds.
  10. Install with plugin/install.

Distribution Patterns

Repo-local marketplace:

repo/
  .agents/
    plugins/
      marketplace.json
  plugins/
    my-plugin/
      .codex-plugin/
        plugin.json
      skills/

Home-local marketplace:

$HOME/
  .agents/
    plugins/
      marketplace.json
  plugins/
    my-plugin/
      .codex-plugin/
        plugin.json

Git marketplace:

marketplace-repo/
  .agents/
    plugins/
      marketplace.json
  plugins/
    my-plugin/
      .codex-plugin/
        plugin.json

Install the Git marketplace:

{
  "method": "marketplace/add",
  "params": {
    "source": "example/codex-marketplace@main",
    "sparsePaths": [".agents", "plugins"]
  }
}

Direct repo discovery:

{
  "method": "plugin/list",
  "params": {
    "cwds": ["/abs/path/to/repo"]
  }
}

Source Files Worth Reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment