Generated banner for Plugin Creator
Generated logo for Plugin Creator

Plugin Creator

OpenAIOfficialPublished by OpenAI

  • Skill
  • Workflow
  • Agent tooling
  • Apache-2.0
  • Low risk
  • Has scripts

From the author

Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, and baseline placeholders you can edit before publishing or testing. Use when Codex needs to create a new local plugin, add optional plugin structure, or generate or update repo-root `.agents/plugins/marketplace.json` entries for plugin ordering and availability metadata.
— the description by OpenAI

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Plugin Creator is a utility for bootstrapping Codex plugins. It automates the creation of the required directory structure, including the mandatory .codex-plugin/plugin.json manifest. It also provides functionality to generate or update the repository-wide marketplace.json file, ensuring plugins are correctly registered for discovery and ordering within the Codex environment. The tool supports optional scaffolding for common plugin components like skills, hooks, and assets.

What it does

  • Generates normalized plugin directory structures.
  • Creates the mandatory .codex-plugin/plugin.json manifest with placeholder values.
  • Updates the repo-root marketplace.json file for plugin registration.
  • Supports optional creation of skills, hooks, scripts, and asset directories.
  • Normalizes plugin names to lower-case hyphen-case.
  • Seeds marketplace metadata with required policy and category fields.

When to use it

  • When starting a new Codex plugin project.
  • When adding new plugin components like skills or hooks to an existing project.
  • When registering a new plugin in the marketplace.json file.
  • When setting up a local development environment for a new plugin.

Advantages

  • Ensures consistent directory structure across all plugins.
  • Reduces manual effort in creating manifest files and marketplace entries.
  • Enforces naming conventions automatically.
  • Provides a clear path for both repo-local and home-local plugin development.

Usage examples

“Create a new plugin named 'Data Analyzer'.”
The tool creates a directory named 'data-analyzer' with the required .codex-plugin/plugin.json file inside.
“Scaffold a new plugin 'my-tool' with skills, hooks, and marketplace entry.”
The tool creates the 'my-tool' directory, adds subdirectories for skills and hooks, and updates marketplace.json with the new entry.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/openai/plugin-creator/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip plugin-creator.zip -d .agents/skills

Documentation by OpenAI

Plugin Creator

Quick Start

  1. Run the scaffold script:

  # Plugin names are normalized to lower-case hyphen-case and must be <= 64 chars.
  # The generated folder and plugin.json name are always the same.
# Run from repo root (or replace .agents/... with the absolute path to this SKILL).
# By default creates in <repo_root>/plugins/<plugin-name>.
python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py <plugin-name>
  1. Open <plugin-path>/.codex-plugin/plugin.json and replace [TODO: ...] placeholders.

  2. Generate or update the repo marketplace entry when the plugin should appear in Codex UI ordering:

# marketplace.json always lives at <repo-root>/.agents/plugins/marketplace.json
python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin --with-marketplace

For a home-local plugin, treat <home> as the root and use:

python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin \
  --path ~/plugins \
  --marketplace-path ~/.agents/plugins/marketplace.json \
  --with-marketplace
  1. Generate/adjust optional companion folders as needed:

python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin --path <parent-plugin-directory> \
  --with-skills --with-hooks --with-scripts --with-assets --with-mcp --with-apps --with-marketplace

<parent-plugin-directory> is the directory where the plugin folder <plugin-name> will be created (for example ~/code/plugins).

What this skill creates

  • If the user has not made the plugin location explicit, ask whether they want a repo-local plugin or a home-local plugin before generating marketplace entries.

  • Creates plugin root at /<parent-plugin-directory>/<plugin-name>/.

  • Always creates /<parent-plugin-directory>/<plugin-name>/.codex-plugin/plugin.json.

  • Fills the manifest with the full schema shape, placeholder values, and the complete interface section.

  • Creates or updates <repo-root>/.agents/plugins/marketplace.json when --with-marketplace is set.

    • If the marketplace file does not exist yet, seed top-level name plus interface.displayName placeholders before adding the first plugin entry.

  • <plugin-name> is normalized using skill-creator naming rules:

    • My Plugin → my-plugin

    • My--Plugin → my-plugin

    • underscores, spaces, and punctuation are converted to -

    • result is lower-case hyphen-delimited with consecutive hyphens collapsed

  • Supports optional creation of:

    • skills/

    • hooks/

    • scripts/

    • assets/

    • .mcp.json

    • .app.json

Marketplace workflow

  • marketplace.json always lives at <repo-root>/.agents/plugins/marketplace.json.

  • For a home-local plugin, use the same convention with <home> as the root: ~/.agents/plugins/marketplace.json plus ./plugins/<plugin-name>.

  • Marketplace root metadata supports top-level name plus optional interface.displayName.

  • Treat plugin order in plugins[] as render order in Codex. Append new entries unless a user explicitly asks to reorder the list.

  • displayName belongs inside the marketplace interface object, not individual plugins[] entries.

  • Each generated marketplace entry must include all of:

    • policy.installation

    • policy.authentication

    • category

  • Default new entries to:

    • policy.installation: "AVAILABLE"

    • policy.authentication: "ON_INSTALL"

  • Override defaults only when the user explicitly specifies another allowed value.

  • Allowed policy.installation values:

    • NOT_AVAILABLE

    • AVAILABLE

    • INSTALLED_BY_DEFAULT

  • Allowed policy.authentication values:

    • ON_INSTALL

    • ON_USE

  • Treat policy.products as an override. Omit it unless the user explicitly requests product gating.

  • The generated plugin entry shape is:

{
  "name": "plugin-name",
  "source": {
    "source": "local",
    "path": "./plugins/plugin-name"
  },
  "policy": {
    "installation": "AVAILABLE",
    "authentication": "ON_INSTALL"
  },
  "category": "Productivity"
}
  • Use --force only when intentionally replacing an existing marketplace entry for the same plugin name.

  • If <repo-root>/.agents/plugins/marketplace.json does not exist yet, create it with top-level "name", an "interface" object containing "displayName", and a plugins array, then add the new entry.

  • For a brand-new marketplace file, the root object should look like:

{
  "name": "[TODO: marketplace-name]",
  "interface": {
    "displayName": "[TODO: Marketplace Display Name]"
  },
  "plugins": [
    {
      "name": "plugin-name",
      "source": {
        "source": "local",
        "path": "./plugins/plugin-name"
      },
      "policy": {
        "installation": "AVAILABLE",
        "authentication": "ON_INSTALL"
      },
      "category": "Productivity"
    }
  ]
}

Required behavior

  • Outer folder name and plugin.json "name" are always the same normalized plugin name.

  • Do not remove required structure; keep .codex-plugin/plugin.json present.

  • Keep manifest values as placeholders until a human or follow-up step explicitly fills them.

  • If creating files inside an existing plugin path, use --force only when overwrite is intentional.

  • Preserve any existing marketplace interface.displayName.

  • When generating marketplace entries, always write policy.installation, policy.authentication, and category even if their values are defaults.

  • Add policy.products only when the user explicitly asks for that override.

  • Keep marketplace source.path relative to repo root as ./plugins/<plugin-name>.

Reference to exact spec sample

For the exact canonical sample JSON for both plugin manifests and marketplace entries, use:

  • references/plugin-json-spec.md

Validation

After editing SKILL.md, run:

python3 <path-to-skill-creator>/scripts/quick_validate.py .agents/skills/plugin-creator

Files

scripts/create_basic_plugin.py

#!/usr/bin/env python3
"""Scaffold a plugin directory and optionally update marketplace.json."""

from __future__ import annotations

import argparse
import json
import re
from pathlib import Path
from typing import Any


MAX_PLUGIN_NAME_LENGTH = 64
DEFAULT_PLUGIN_PARENT = Path.cwd() / "plugins"
DEFAULT_MARKETPLACE_PATH = Path.cwd() / ".agents" / "plugins" / "marketplace.json"
DEFAULT_INSTALL_POLICY = "AVAILABLE"
DEFAULT_AUTH_POLICY = "ON_INSTALL"
DEFAULT_CATEGORY = "Productivity"
DEFAULT_MARKETPLACE_DISPLAY_NAME = "[TODO: Marketplace Display Name]"
VALID_INSTALL_POLICIES = {"NOT_AVAILABLE", "AVAILABLE", "INSTALLED_BY_DEFAULT"}
VALID_AUTH_POLICIES = {"ON_INSTALL", "ON_USE"}


def normalize_plugin_name(plugin_name: str) -> str:
    """Normalize a plugin name to lowercase hyphen-case."""
    normalized = plugin_name.strip().lower()
    normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
    normalized = normalized.strip("-")
    normalized = re.sub(r"-{2,}", "-", normalized)
    return normalized


def validate_plugin_name(plugin_name: str) -> None:
    if not plugin_name:
        raise ValueError("Plugin name must include at least one letter or digit.")
    if len(plugin_name) > MAX_PLUGIN_NAME_LENGTH:
        raise ValueError(
            f"Plugin name '{plugin_name}' is too long ({len(plugin_name)} characters). "
            f"Maximum is {MAX_PLUGIN_NAME_LENGTH} characters."
        )


def build_plugin_json(plugin_name: str) -> dict:
    return {
        "name": plugin_name,
        "version": "[TODO: 1.2.0]",
        "description": "[TODO: Brief plugin description]",
        "author": {
            "name": "[TODO: Author Name]",
            "email": "[TODO: [email protected]]",
            "url": "[TODO: https://github.com/author]",
        },
        "homepage": "[TODO: https://docs.example.com/plugin]",
        "repository": "[TODO: https://github.com/author/plugin]",
        "license": "[TODO: MIT]",
        "keywords": ["[TODO: keyword1]", "[TODO: keyword2]"],
        "skills": "[TODO: ./skills/]",
        "hooks": "[TODO: ./hooks.json]",
        "mcpServers": "[TODO: ./.mcp.json]",
        "apps": "[TODO: ./.app.json]",
        "interface": {
            "displayName": "[TODO: Plugin Display Name]",
            "shortDescription": "[TODO: Short description for subtitle]",
            "longDescription": "[TODO: Long description for details page]",
            "developerName": "[TODO: OpenAI]",
            "category": "[TODO: Productivity]",
            "capabilities": ["[TODO: Interactive]", "[TODO: Write]"],
            "websiteURL": "[TODO: https://openai.com/]",
            "privacyPolicyURL": "[TODO: https://openai.com/policies/row-privacy-policy/]",
            "termsOfServiceURL": "[TODO: https://openai.com/policies/row-terms-of-use/]",
            "defaultPrompt": [
                "[TODO: Summarize my inbox and draft replies for me.]",
                "[TODO: Find open bugs and turn them into tickets.]",
                "[TODO: Review today's meetings and flag gaps.]",
            ],
            "brandColor": "[TODO: #3B82F6]",
            "composerIcon": "[TODO: ./assets/icon.png]",
            "logo": "[TODO: ./assets/logo.png]",
            "screenshots": [
                "[TODO: ./assets/screenshot1.png]",
                "[TODO: ./assets/screenshot2.png]",
                "[TODO: ./assets/screenshot3.png]",
            ],
        },
    }


def build_marketplace_entry(
    plugin_name: str,
    install_policy: str,
    auth_policy: str,
    category: str,
) -> dict[str, Any]:
    return {
        "name": plugin_name,
        "source": {
            "source": "local",
            "path": f"./plugins/{plugin_name}",
        },
        "policy": {
            "installation": install_policy,
            "authentication": auth_policy,
        },
        "category": category,
    }


def load_json(path: Path) -> dict[str, Any]:
    with path.open() as handle:
        return json.load(handle)


def build_default_marketplace() -> dict[str, Any]:
    return {
        "name": "[TODO: marketplace-name]",
        "interface": {
            "displayName": DEFAULT_MARKETPLACE_DISPLAY_NAME,
        },
        "plugins": [],
    }


def validate_marketplace_interface(payload: dict[str, Any]) -> None:
    interface = payload.get("interface")
    if interface is not None and not isinstance(interface, dict):
        raise ValueError("marketplace.json field 'interface' must be an object.")


def update_marketplace_json(
    marketplace_path: Path,
    plugin_name: str,
    install_policy: str,
    auth_policy: str,
    category: str,
    force: bool,
) -> None:
    if marketplace_path.exists():
        payload = load_json(marketplace_path)
    else:
        payload = build_default_marketplace()

    if not isinstance(payload, dict):
        raise ValueError(f"{marketplace_path} must contain a JSON object.")

    validate_marketplace_interface(payload)

    plugins = payload.setdefault("plugins", [])
    if not isinstance(plugins, list):
        raise ValueError(f"{marketplace_path} field 'plugins' must be an array.")

    new_entry = build_marketplace_entry(plugin_name, install_policy, auth_policy, category)

    for index, entry in enumerate(plugins):
        if isinstance(entry, dict) and entry.get("name") == plugin_name:
            if not force:
                raise FileExistsError(
                    f"Marketplace entry '{plugin_name}' already exists in {marketplace_path}. "
                    "Use --force to overwrite that entry."
                )
            plugins[index] = new_entry
            break
    else:
        plugins.append(new_entry)

    write_json(marketplace_path, payload, force=True)


def write_json(path: Path, data: dict, force: bool) -> None:
    if path.exists() and not force:
        raise FileExistsError(f"{path} already exists. Use --force to overwrite.")
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w") as handle:
        json.dump(data, handle, indent=2)
        handle.write("\n")


def create_stub_file(path: Path, payload: dict, force: bool) -> None:
    if path.exists() and not force:
        return
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w") as handle:
        json.dump(payload, handle, indent=2)
        handle.write("\n")


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Create a plugin skeleton with placeholder plugin.json."
    )
    parser.add_argument("plugin_name")
    parser.add_argument(
        "--path",
        default=str(DEFAULT_PLUGIN_PARENT),
        help=(
            "Parent directory for plugin creation (defaults to <cwd>/plugins). "
            "When using a home-rooted marketplace, use <home>/plugins."
        ),
    )
    parser.add_argument("--with-skills", action="store_true", help="Create skills/ directory")
    parser.add_argument("--with-hooks", action="store_true", help="Create hooks/ directory")
    parser.add_argument("--with-scripts", action="store_true", help="Create scripts/ directory")
    parser.add_argument("--with-assets", action="store_true", help="Create assets/ directory")
    parser.add_argument("--with-mcp", action="store_true", help="Create .mcp.json placeholder")
    parser.add_argument("--with-apps", action="store_true", help="Create .app.json placeholder")
    parser.add_argument(
        "--with-marketplace",
        action="store_true",
        help=(
            "Create or update <cwd>/.agents/plugins/marketplace.json. "
            "Marketplace entries always point to ./plugins/<plugin-name> relative to the "
            "marketplace root."
        ),
    )
    parser.add_argument(
        "--marketplace-path",
        default=str(DEFAULT_MARKETPLACE_PATH),
        help=(
            "Path to marketplace.json (defaults to <cwd>/.agents/plugins/marketplace.json). "
            "For a home-rooted marketplace, use <home>/.agents/plugins/marketplace.json."
        ),
    )
    parser.add_argument(
        "--install-policy",
        default=DEFAULT_INSTALL_POLICY,
        choices=sorted(VALID_INSTALL_POLICIES),
        help="Marketplace policy.installation value",
    )
    parser.add_argument(
        "--auth-policy",
        default=DEFAULT_AUTH_POLICY,
        choices=sorted(VALID_AUTH_POLICIES),
        help="Marketplace policy.authentication value",
    )
    parser.add_argument(
        "--category",
        default=DEFAULT_CATEGORY,
        help="Marketplace category value",
    )
    parser.add_argument("--force", action="store_true", help="Overwrite existing files")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    raw_plugin_name = args.plugin_name
    plugin_name = normalize_plugin_name(raw_plugin_name)
    if plugin_name != raw_plugin_name:
        print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.")
    validate_plugin_name(plugin_name)

    plugin_root = (Path(args.path).expanduser().resolve() / plugin_name)
    plugin_root.mkdir(parents=True, exist_ok=True)

    plugin_json_path = plugin_root / ".codex-plugin" / "plugin.json"
    write_json(plugin_json_path, build_plugin_json(plugin_name), args.force)

    optional_directories = {
        "skills": args.with_skills,
        "hooks": args.with_hooks,
        "scripts": args.with_scripts,
        "assets": args.with_assets,
    }
    for folder, enabled in optional_directories.items():
        if enabled:
            (plugin_root / folder).mkdir(parents=True, exist_ok=True)

    if args.with_mcp:
        create_stub_file(
            plugin_root / ".mcp.json",
            {"mcpServers": {}},
            args.force,
        )

    if args.with_apps:
        create_stub_file(
            plugin_root / ".app.json",
            {
                "apps": {},
            },
            args.force,
        )

    if args.with_marketplace:
        marketplace_path = Path(args.marketplace_path).expanduser().resolve()
        update_marketplace_json(
            marketplace_path,
            plugin_name,
            args.install_policy,
            args.auth_policy,
            args.category,
            args.force,
        )

    print(f"Created plugin scaffold: {plugin_root}")
    print(f"plugin manifest: {plugin_json_path}")
    if args.with_marketplace:
        print(f"marketplace manifest: {marketplace_path}")


if __name__ == "__main__":
    main()

Requirements and compatibility

Written for: OpenAI Codex

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
OpenAI
Publisher
OpenAI (Official)
Source
openai/skills · skills/.system/plugin-creator · commit 49f948f
Licence
Apache License 2.0 (redistribution allowed)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by OpenAI.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download plugin-creator.zip