Building a Codex CLI Plugin: Skills, Hooks, MCP Servers and Project-Specific Automation
Building a Codex CLI Plugin: Skills, Hooks, MCP Servers and Project-Specific Automation
Codex CLI plugins — introduced in March 2026 — are installable bundles that package skills, app integrations, and MCP server configurations into reusable, distributable units1. A well-crafted plugin transforms a generic coding agent into a project-aware automation engine that enforces your team’s conventions, connects to your enterprise services, and gates risky operations behind deterministic checks.
This article walks through building a complete plugin for a typical enterprise project, covering every component from skill authoring through marketplace distribution.
Plugin Anatomy
Every plugin is a directory with a required manifest at .codex-plugin/plugin.json and optional supporting files2:
my-project-plugin/
├── .codex-plugin/
│ └── plugin.json # Required: manifest
├── skills/
│ ├── migration-generator/
│ │ └── SKILL.md
│ ├── pr-description/
│ │ └── SKILL.md
│ └── deploy-preflight/
│ ├── SKILL.md
│ └── scripts/
│ └── check-env.sh
├── .mcp.json # Optional: MCP server config
├── .app.json # Optional: app integrations
└── assets/
└── icon.png # Optional: branding
The manifest declares metadata and points to each component:
{
"name": "acme-service-plugin",
"version": "1.0.0",
"description": "Project automation for ACME microservices",
"author": {
"name": "ACME Platform Team",
"email": "platform@acme.dev"
},
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"apps": "./.app.json",
"interface": {
"displayName": "ACME Service Plugin",
"shortDescription": "Skills, hooks, and MCP for ACME projects",
"category": "Engineering",
"capabilities": ["Read", "Write"],
"brandColor": "#2563EB"
}
}
Critical rule: only plugin.json belongs inside .codex-plugin/. Everything else lives at the plugin root2.
Project-Specific Skills
Skills are the primary unit of reusable automation. Each skill is a directory containing a SKILL.md file with YAML frontmatter declaring name and description (both required), followed by Markdown instructions3.
Skill Discovery
Codex scans multiple hierarchical locations for skills, from most specific to broadest3:
graph TD
A["$CWD/.agents/skills"] -->|Repo scoped| B["$REPO_ROOT/.agents/skills"]
B --> C["~/.agents/skills"]
C -->|User scoped| D["/etc/codex/skills"]
D -->|Admin scoped| E["Built-in skills"]
E -->|System| F["Plugin-bundled skills"]
style A fill:#dbeafe
style F fill:#dbeafe
When packaged inside a plugin, skills are discovered via the "skills" pointer in plugin.json and cached at ~/.codex/plugins/cache/$MARKETPLACE/$PLUGIN/$VERSION/2.
Worked Example: Migration Generator
A skill that reads your ORM schema and generates a migration file following team conventions:
---
name: migration-generator
description: >
Generate a database migration from the current ORM schema diff.
Trigger when the user asks to create, add, or modify a database migration.
Do NOT trigger for seed data or fixture generation.
---
## Steps
1. Read the ORM schema from `src/db/schema.prisma` (or the equivalent for the project's ORM).
2. Compare against the latest migration in `prisma/migrations/`.
3. Generate a new migration using the project's migration tool (`npx prisma migrate dev --name <descriptive-name>`).
4. Validate the generated SQL against the team's migration checklist in `docs/MIGRATION_RULES.md`.
5. If the migration includes destructive operations (DROP, ALTER column type), add a `-- DESTRUCTIVE` comment header and warn the user.
Worked Example: PR Description Writer
---
name: pr-description
description: >
Generate a pull request description following the team's ADR template.
Trigger when the user asks to write, draft, or create a PR description.
---
## Steps
1. Run `git diff main...HEAD --stat` to identify changed files.
2. Read `docs/templates/PR_TEMPLATE.md` for the required sections.
3. For each changed module, summarise the intent (not the diff).
4. Include a "## Breaking Changes" section if any public API signatures changed.
5. Add a "## Testing" section listing which test suites cover the changes.
Controlling Implicit Invocation
For skills that should only fire when explicitly requested, add an agents/openai.yaml file3:
policy:
allow_implicit_invocation: false
This prevents the migration generator from firing when someone merely mentions “database” in a prompt.
Hooks as Quality Gates
Hooks inject deterministic scripts into the Codex agent loop. They are currently behind a feature flag4:
[features]
codex_hooks = true
Hook configuration lives in hooks.json files at two scopes — ~/.codex/hooks.json (user-level) and <repo>/.codex/hooks.json (repository-level). Both load additively; higher-precedence layers do not replace lower-precedence hooks4.
Hook Lifecycle Events
Codex supports five hook events4:
| Event | When It Fires | Matcher Filters On |
|---|---|---|
SessionStart |
Session starts or resumes | startup or resume |
PreToolUse |
Before a tool executes | Tool name (currently Bash) |
PostToolUse |
After a tool executes | Tool name |
UserPromptSubmit |
User submits a prompt | Not supported |
Stop |
Agent stops | Not supported |
sequenceDiagram
participant User
participant Codex
participant PreHook as PreToolUse Hook
participant Tool as Bash
participant PostHook as PostToolUse Hook
User->>Codex: Submit prompt
Codex->>PreHook: Check command policy
alt Denied
PreHook-->>Codex: Exit 2 + reason
Codex-->>User: Command blocked
else Approved
PreHook-->>Codex: Exit 0
Codex->>Tool: Execute command
Tool-->>Codex: Result
Codex->>PostHook: Review output
PostHook-->>Codex: additionalContext
end
Pre-Tool Hook: Blocking Destructive Commands
A PreToolUse hook that blocks risky operations on protected paths:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .codex/hooks/guard_infra.py",
"statusMessage": "Checking infrastructure policy",
"timeout": 10
}
]
}
]
}
}
The guard script receives JSON on stdin with the proposed command in tool_input.command. To deny execution, exit with code 2 and print the reason to stderr4:
#!/usr/bin/env python3
import json, sys, re
data = json.load(sys.stdin)
cmd = data.get("tool_input", {}).get("command", "")
PROTECTED = [r"infra/", r"terraform/", r"\.env", r"secrets/"]
if any(re.search(p, cmd) for p in PROTECTED):
print("Blocked: command touches protected infrastructure paths. "
"Require human approval.", file=sys.stderr)
sys.exit(2)
Stop Hook: Enforcing Test Passage
A Stop hook that forces another pass when tests are failing:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 .codex/hooks/check_tests.py",
"timeout": 120
}
]
}
]
}
}
The script returns a JSON decision: "block" response to trigger continuation4:
#!/usr/bin/env python3
import json, subprocess, sys
result = subprocess.run(["npm", "test"], capture_output=True, text=True)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": "Tests are still failing. Fix the remaining failures before stopping."
}))
sys.exit(0)
MCP Server Integration
The .mcp.json file (or direct config.toml entries) connects the plugin to enterprise services5. Two transport types are supported: STDIO for local processes and Streamable HTTP for remote servers5.
Connecting to Enterprise Services
{
"mcp_servers": {
"jira": {
"url": "https://mcp.atlassian.com/jira",
"bearer_token_env_var": "JIRA_API_TOKEN"
},
"datadog": {
"command": "npx",
"args": ["-y", "@datadog/mcp-server"],
"env": {
"DD_API_KEY": "${DD_API_KEY}",
"DD_APP_KEY": "${DD_APP_KEY}"
}
},
"openai-docs": {
"url": "https://developers.openai.com/mcp"
}
}
}
This gives skills access to sprint backlogs (Jira), runtime metrics during debugging (Datadog), and official documentation (OpenAI Docs)5. Each MCP server’s tools can be selectively enabled or disabled via enabled_tools and disabled_tools arrays in the server configuration5.
Timeout Configuration
For enterprise servers behind VPNs or with higher latency, adjust timeouts5:
[mcp_servers.jira]
url = "https://mcp.internal.acme.dev/jira"
startup_timeout_sec = 30
tool_timeout_sec = 120
AGENTS.md Composition
Codex discovers AGENTS.md files hierarchically, walking from the project root down to the current working directory. The closest file to the edited file takes precedence; explicit user prompts override everything6.
A plugin can ship AGENTS.md fragments at directory level without overwriting existing team instructions. The recommended pattern:
my-project/
├── AGENTS.md # Team-wide conventions
├── src/
│ ├── AGENTS.md # Source code conventions
│ └── db/
│ └── AGENTS.md # Plugin-provided: migration rules
└── infra/
└── AGENTS.md # Plugin-provided: IaC conventions
For local overrides that should not be committed, use AGENTS.override.md — Codex merges these with the base AGENTS.md at the same directory level6.
Fallback filenames and size limits are configurable in ~/.codex/config.toml6:
project_doc_fallback_filenames = ["TEAM_GUIDE.md", ".agents.md"]
project_doc_max_bytes = 65536
Marketplace Distribution
Plugins are distributed through marketplace JSON files at three scopes2:
| Scope | Location |
|---|---|
| Repository | $REPO_ROOT/.agents/plugins/marketplace.json |
| Personal | ~/.agents/plugins/marketplace.json |
| Official | OpenAI Plugin Directory (coming soon) |
Repository Marketplace
For team-internal distribution, commit a marketplace file alongside the plugin:
{
"name": "acme-internal-plugins",
"interface": {
"displayName": "ACME Internal Plugins"
},
"plugins": [
{
"name": "acme-service-plugin",
"source": {
"source": "local",
"path": "./plugins/acme-service-plugin"
},
"policy": {
"installation": "INSTALLED_BY_DEFAULT",
"authentication": "ON_INSTALL"
},
"category": "Engineering"
}
]
}
Setting installation to INSTALLED_BY_DEFAULT ensures every team member gets the plugin without manual setup2. Three installation policies are available: AVAILABLE, INSTALLED_BY_DEFAULT, and NOT_AVAILABLE2.
Installing Locally
# Repository-scoped
mkdir -p ./plugins
cp -R /path/to/acme-service-plugin ./plugins/acme-service-plugin
# Personal
mkdir -p ~/.codex/plugins
cp -R /path/to/acme-service-plugin ~/.codex/plugins/acme-service-plugin
Codex caches installed plugins at ~/.codex/plugins/cache/$MARKETPLACE_NAME/$PLUGIN_NAME/$VERSION/. For local plugins, $VERSION is local2.
Testing the Plugin
Validate each skill against fixture inputs using codex exec:
# Test migration generator against a known schema diff
codex exec --skill ./skills/migration-generator \
--input "Generate a migration adding a 'status' column to the orders table"
# Test PR description against a known diff
git checkout feature-branch
codex exec --skill ./skills/pr-description \
--input "Write a PR description for the current branch"
For CI integration, run skills in a locked-down profile with workspace-write sandbox mode to prevent unintended side effects. ⚠️ The exact codex exec flags may vary — consult your installed version’s help output.
The Complete Lifecycle
graph LR
A[Author skills] --> B[Configure hooks]
B --> C[Wire MCP servers]
C --> D[Write plugin.json manifest]
D --> E[Add to marketplace.json]
E --> F[Test with codex exec]
F --> G[Commit to repo]
G --> H[Team installs via /plugins]
style A fill:#dbeafe
style H fill:#bbf7d0
The plugin pattern gives teams a single distributable unit that encodes project knowledge — from coding conventions in AGENTS.md through quality gates in hooks to enterprise service access via MCP. As the official Plugin Directory matures, expect cross-organisation sharing of these bundles to become the primary way teams bootstrap Codex CLI for new projects.
Citations
-
OpenAI introduces plugin support in Codex — AlternativeTo, March 2026 ↩
-
Build plugins — Codex Developer Documentation — OpenAI, 2026 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Agent Skills — Codex Developer Documentation — OpenAI, 2026 ↩ ↩2 ↩3
-
Hooks — Codex Developer Documentation — OpenAI, 2026 ↩ ↩2 ↩3 ↩4 ↩5
-
Model Context Protocol — Codex Developer Documentation — OpenAI, 2026 ↩ ↩2 ↩3 ↩4 ↩5
-
Custom instructions with AGENTS.md — Codex Developer Documentation — OpenAI, 2026 ↩ ↩2 ↩3