Chapter 18: Skills, Plugins, Connectors, and Typed Extensions
Reading Contract: Use this chapter to answer one question: when a Codex turn is shaped by extensions, which layer contributed instruction, packaging, hosted-app metadata, or compiled prompt state? Track four planes: skills, plugins, connectors, and typed contributors. Afterward, you should be able to tell whether a capability is being summarized for discovery, installed from a package, gated by account access, or compiled into the process.

Source boundary: named files, structs, enums, constants, schemas, and loading functions are verified source only where this chapter links to the pinned Codex commit or this chapter’s Source Map. Claims about “extension planes” and “trust boundaries” are surrounding contract inference from those visible owners. This chapter does not claim that every plugin marketplace, hosted app, or typed contributor shares one hidden runtime.
Chapter 17 treated MCP as the protocol boundary for external tools. This chapter moves up a layer. Before a tool can be routed, Codex must decide which workflow instructions, package contributions, hosted app identities, and in-process prompt fragments are allowed to shape a thread.
You are here: external tool calls now have a clean provenance and routing boundary.
Problem: an agent platform needs reusable instructions, packaged assets, hosted app metadata, and compiled contributors, but each carries a different trust model and lifecycle.
Mental model: Codex has four converging extension planes: skills teach behavior, plugins distribute contributions, connectors describe hosted app access, and typed extensions contribute through explicit in-process APIs.
These surfaces are related, but they are not interchangeable. A skill is primarily an instruction/workflow unit. A plugin is a distribution package that can contribute skills, MCP server definitions, app connector IDs, hooks, and interface metadata. A connector is hosted app directory metadata plus account-scoped accessibility. A typed extension is compiled into the process and contributes through a narrow API.
The architecture becomes readable when each plane answers a different question:
| Plane | Question it answers | Owner visible in source | Boundary pressure |
|---|---|---|---|
| Skills | Which instructions and workflow files are available? | skills manager and metadata model | prompt budget and invocation discipline |
| Plugins | How do contribution assets arrive and stay scoped? | plugin manifest and plugin manager | package trust, path validation, sync failure |
| Connectors | Which hosted apps are known and account-accessible? | connector directory cache and app metadata | directory knowledge versus usable capability |
| Typed extensions | Which prompt fragments can compiled code add? | extension API types | compile-time contract instead of dynamic code loading |
The invariant is that no plane should silently become all the others. A plugin may ship a skill root, but the skill loader still decides how skills are discovered and disabled. A connector may describe an app, but account access still decides whether tools are usable. A typed prompt fragment may contribute text, but it does so through explicit slots, not arbitrary runtime mutation.
1. Skills: Instruction With A Budget
A skill is a named body of instructions, metadata, policy, optional interface data, optional dependencies, and a path to the declaring SKILL.md. The source model is explicit: SkillMetadata includes name, description, interface, dependencies, policy, path_to_skills_md, scope, and optional plugin_id. SkillLoadOutcome separates loaded skills, errors, disabled paths, roots, file systems, and indexes for implicit invocation.
Shape-level, a loaded skill is not just text:
{
"metadata": {
"name": "review-pr",
"description": "Review a pull request",
"scope": "repo",
"plugin_id": null,
"policy": { "allow_implicit_invocation": true }
},
"load_outcome": {
"enabled": true,
"implicit_indexes": ["scripts dir", "doc path"],
"errors": []
}
}
This is why a skill budget exists. If every skill body were always placed in the model view, “extension” would become permanent prompt growth. The verified source does not require us to know every prompt-rendering detail to see the lifecycle: discover roots, load metadata, filter by product and disabled paths, build implicit indexes, and cache the outcome.
1.1 Discovery, Disablement, And Implicit Invocation Are Separate Phases
SkillsManager::skills_for_config() caches by effective skill-relevant config state so overrides do not bleed across sessions that share a directory. build_skill_outcome() loads from roots, filters by product, resolves disabled skill paths, and finalizes implicit indexes. finalize_skill_outcome() then builds the script/doc-path indexes from skills allowed for implicit invocation.
Those phases protect three different invariants:
| Phase | Runtime pressure | Protected invariant |
|---|---|---|
| root selection | system, user, admin, repo, and plugin roots can overlap | scope is explicit and cache keys include roots |
| metadata loading | invalid skill files should not become instructions | errors remain load outcomes, not prompt text |
| disablement | user/config policy can suppress a skill | disabled paths are retained and checked |
| implicit indexing | a workflow may trigger without being named | only enabled skills whose policy allows implicit invocation are indexed |
The simpler design would concatenate every discovered instruction. That fails because it gives invalid, disabled, or irrelevant workflows the same authority as the requested task.

2. Plugins: Packages Are Classifiers, Not Runtime Authority
Plugins answer a different question: how do extension assets arrive, remain scoped to a package, and contribute to other planes? The manifest model makes this visible. PluginManifest has identity and interface fields plus PluginManifestPaths. Those paths can point to skills, mcp_servers, apps, and hooks.
Shape-level, a plugin manifest contribution is a classified package boundary:
{
"name": "example-plugin",
"paths": {
"skills": "./skills",
"mcpServers": "./mcp.json",
"apps": "./apps.json",
"hooks": "./hooks.json"
},
"interface": {
"displayName": "Example",
"defaultPrompt": ["Start with the plugin's workflow"]
}
}
The loader should not treat those strings as filesystem authority. resolve_manifest_path() requires paths to start with ./, rejects empty ./, rejects .., and rejects components that leave the plugin root. Interface default prompts are also bounded: the manifest code caps the count and length before accepting them.
2.1 Plugin Sync Is A Degraded State, Not A Thread Blocker
The plugin manager source shows a second pressure: remote state can fail independently of a thread. PluginsManager owns a plugin store, marketplace caches, remote installed plugin caches, a remote sync lock, a cached enabled outcome, and product restriction context. plugins_for_config_with_force_reload() returns a cached outcome when the effective config version and hook setting match, otherwise it loads configured plugins and records load errors.
That is the right lifecycle for optional packaging infrastructure:
plugin config
-> local store / remote cache
-> manifest parse and path validation
-> contribution classification
-> skill roots, MCP configs, app IDs, hooks
-> downstream loaders apply their own policy
The plugin layer classifies and validates contributions. It should not silently make a hook trusted, a skill enabled, or a hosted app account-accessible.
3. Connectors: Directory Knowledge Is Not Usable Capability
Connectors normalize hosted app directory metadata. The connector source begins with a cache key that includes base URL, account ID, ChatGPT user ID, and whether the account is a workspace account. ConnectorDirectoryCacheKey is therefore account-scoped, not a global app catalog key.
list_all_connectors_with_options() also shows the directory lifecycle: use unexpired in-memory cache unless forced, fetch directory and workspace pages, merge apps, convert them to AppInfo, normalize names/descriptions, write cache, and set is_accessible to false at this directory stage.
That last detail is the boundary. A connector can be known before it is usable.
| State | What Codex knows | What it must not assume |
|---|---|---|
| directory connector | app ID, display metadata, logo/branding, install URL | that current credentials can call tools |
| accessible connector | account-specific access information | that every tool should be direct in the model request |
| hosted app MCP tool | connector-backed tool metadata | that origin/provenance is the same as a local MCP server |
This is why Chapter 17’s MCP exposure had to filter hosted app MCP tools by connector and app-tool enablement. The connector plane supplies directory and access facts; the model-facing tool boundary is later.

4. Typed Extensions: Compile-Time Slots Instead Of Dynamic Code
Typed extensions are the least dynamic plane. The prompt contributor API is small enough to quote conceptually: PromptSlot enumerates slots such as DeveloperPolicy, DeveloperCapabilities, ContextualUser, and SeparateDeveloper. PromptFragment carries only a slot and model-visible text.
Shape-level:
{
"slot": "DeveloperCapabilities",
"text": "A compiled contributor may add a bounded capability note here."
}
That narrow shape is the value. Typed extensions are useful when the extension author and runtime author share a compile-time contract. They are not a replacement for plugins because they do not solve distribution, marketplace sync, plugin-relative assets, or hosted app access. They solve a different problem: in-process contribution without arbitrary runtime code loading.
5. The Loading Pattern: Strict At The Boundary, Fail-Soft Outside It
The four planes age well when each validates its own boundary and degrades in its own vocabulary.
| Plane | Pressure source | Why the simpler approach fails | Mechanism | Failure boundary |
|---|---|---|---|---|
| skills | many possible instructions | always loading full bodies bloats the model view | metadata, disabled paths, implicit indexes | invalid/disabled skills stay out of prompt authority |
| plugins | packages contain paths and remote state | trusting manifest strings grants filesystem authority | ./ path validation, cache/load outcomes | invalid paths or sync errors become warnings/outcomes |
| connectors | apps exist before access is proven | showing directory apps as usable tools lies to users | account-scoped cache and access separation | unauthenticated apps stay unavailable |
| typed extensions | internal integrations need prompt hooks | dynamic code loading weakens runtime ownership | typed slots and PromptFragment | incompatible data is rejected at API/build boundary |
The common invariant is ownership. Skills own workflow instruction. Plugins own package classification. Connectors own hosted app metadata and account access. Typed extensions own compiled prompt contributions. The turn should see governed context and tools, not the accidental details of how every extension arrived.
Trace Ledger
| Question | Chapter 18 answer |
|---|---|
| Where is the user request now? | It is being shaped by extension context before and during thread construction and tool exposure. |
| What carries it? | SkillMetadata, SkillLoadOutcome, plugin manifests, plugin load outcomes, connector directory entries, MCP configs, hook configs, app connector IDs, and typed prompt fragments. |
| Who owns the next decision? | Skill policy and loaders decide instruction availability; plugin loading classifies package contributions; connector access gates hosted tools; typed APIs accept only compiled fragments; the model acts after those decisions. |
| What must remain invariant? | Package identity must not become filesystem authority, directory metadata must not become account access, and summaries/metadata must not silently replace full source-bound instructions when full-body loading is required. |
| What can fail here? | invalid metadata, disabled skills, untrusted plugin paths, stale remote caches, plugin sync errors, missing connector access, unavailable hosted app tools, or incompatible typed data. |
Apply This
- Name the plane first. Use this when a capability arrives from an extension. Decide whether it is instruction, package contribution, hosted app metadata, or compiled prompt state. Pitfall: giving every package format equal runtime authority.
- Separate discovery from activation. Use this for skills and connectors. A skill can be known but disabled; a connector can be listed but inaccessible. Pitfall: treating “known” as “active in this turn.”
- Validate package-relative paths. Use this for plugins. Require paths to remain under the package root before registering contributions. Pitfall: trusting manifest strings as filesystem authority.
- Cache by the real owner. Use this for skill and connector state. Include config roots or account identity in cache keys. Pitfall: sharing stale extension state across sessions with different policy.
- Prefer typed slots for compiled contributions. Use this for in-process integrations. Contribute small, explicit records instead of dynamic runtime code. Pitfall: using typed extensions as a plugin ABI.
What Comes Next
Extension loading handles native capability surfaces. Chapter 19 asks a harder compatibility question: what happens when users bring configurations and sessions from another agent system whose semantics do not perfectly match Codex?
Source Map
| Concept | Source anchor |
|---|---|
| Skills manager | codex-rs/core-skills/src/manager.rs |
| Skill metadata model | codex-rs/core-skills/src/model.rs |
| Plugin manifest and path validation | codex-rs/core-plugins/src/manifest.rs |
| Plugin manager | codex-rs/core-plugins/src/manager.rs |
| Connector directory model | codex-rs/connectors/src/lib.rs |
| Typed prompt extension API | codex-rs/ext/extension-api/src/contributors/prompt.rs |