Migrate To Codex
OpenAIOfficialPublished by OpenAI
- Skill
- Workflow
- Automation and integrations
- Apache-2.0
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The migrate-to-codex skill provides a structured, automated procedure to migrate existing Claude Code setups into the Codex ecosystem. It handles the conversion of instruction files, slash commands, skills, agents, and MCP server configurations. The tool follows a rigorous process involving scanning, planning, dry-running, and validating the migration to ensure artifacts are correctly mapped while preserving project-specific configurations. It includes a self-healing loop to resolve manual migration requirements and ensures that generated Codex artifacts are validated against target standards.
What it does
- Converts Claude Code instruction files into Codex-compatible formats.
- Migrates slash commands and skills into the Codex skill structure.
- Updates MCP server configurations for use in Codex.
- Generates a migration report detailing added items and manual review requirements.
- Validates generated Codex artifacts to ensure configuration integrity.
- Provides a dry-run mode to preview changes before applying them.
When to use it
- When transitioning a development environment from Claude Code to Codex.
- When you need to port custom skills or agents to a new Codex project.
- When updating global or project-level configurations to align with Codex standards.
- When you need to verify the readiness of existing Claude artifacts for Codex.
Advantages
- Automates complex migration steps, reducing manual effort.
- Provides clear reporting on what was successfully migrated and what requires manual attention.
- Includes a self-healing loop to iteratively resolve migration issues.
- Ensures project-specific configurations are preserved during the transition.
- Validates targets post-migration to prevent configuration errors.
Usage examples
- “Run a scan to see what Claude artifacts are available for migration.”
- The tool lists active and inactive source surfaces from the specified Claude directory.
- “Perform a dry-run migration of my project settings to Codex.”
- The tool generates a plan and reports potential changes without modifying any files.
- “Execute the full migration of my global Claude configuration to Codex.”
- The tool migrates instructions, skills, and MCP configs, creating a report of the results.
- “Validate the Codex target after my migration.”
- The tool checks the generated Codex files for errors and reports any remaining manual fixes.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/openai/migrate-to-codex/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip migrate-to-codex.zip -d .agents/skills
Documentation by OpenAI
Migrate to Codex
Autonomy
Keep going until the selected migration is completely done: run the migrator, inspect the report, fix migrated Codex instructions/skills/agents/MCP config, and re-run checks without stopping to ask for confirmation of the next step. If the user has selected a target, do not ask before creating, editing, replacing, or deleting generated Codex artifacts in that target (AGENTS.md, .codex/, .agents/, or ~/.codex/). Preserve unrelated existing Codex config entries in .codex/config.toml or ~/.codex/config.toml, such as notify, projects, marketplaces, or unrelated MCP servers; do not ask about them unless they fail validation or directly conflict with the migration. Do not edit source Claude Code files (.claude/, ~/.claude/, .mcp.json, or .claude.json), unrelated project code, secrets, or another repository.
Migration Order
Run the migration in this order for each selected global or project source:
Start by using Codex's built-in TODO/task list tool. Do not create
MIGRATION_TODOS.mdor any TODO file unless the user explicitly asks. The TODO list input has aplanarray whose items each havestepandstatus; use statusespending,in_progress, andcompleted. Make the TODOs specific to the selected artifacts. Before finishing, update the TODO list so every finished step is markedcompletedand no step remainsin_progress. Use literal source → Codex target labels, for example:Inspect
.claude/commands→ Codex skills/promptsInspect
.claude/agents→.codex/agentsInspect
.mcp.json→.codex/config.tomlMCP serversInspect
.claude/settings.jsonhooks →.codex/hooks.jsonMigrate safe selected artifacts → Codex files
Validate generated
.codex/config.tomlValidate generated
.codex/agentsReport migrated artifacts and manual-review items
Read
references/differences.md(and refresh Codex docs if itsDocs last checkeddate is old).Scan and inspect before writing:
--scan-onlylists active and inactive source surfaces.--planprints staged Codex artifact paths and report rows.--doctorsummarizes readiness, manual-review work, and validation risks.
Convert surfaces in the same order the CLI uses:
instructions:
CLAUDE.md/AGENTS.mdtoAGENTS.mdplugins: report Claude plugin trees and marketplaces as manual migration work
hooks: rewrite supported Claude hooks into
.codex/hooks.jsonand enable[features].codex_hooks = trueskills and commands: write Codex skills under
.agents/skills/config: write
.codex/config.tomlfrom Claude model/sandbox settings and MCP servers, includingpersonality = "friendly"when config is generatedsubagents: write Codex custom agents under
.codex/agents/
Dry-run, then write the selected target. Use
--replaceonly when orphan generated skills or agents should be deleted.Inspect the terminal output and
.codex/migrate-to-codex-report.txtafter real runs.Review generated artifacts in this order:
AGENTS.md,.agents/skills/,.codex/config.toml,.codex/hooks.json,.codex/agents/, then report-only plugin items.Run
--validate-targetagainst each target after edits.Re-run checks and
--dry-runafter edits.Return the final migration report as one markdown table per scope that has rows. The tables cover only the non-native follow-up migration work you performed, such as skills created from slash commands, subagents, MCP servers, hooks, unsupported/local plugin notes, and manual-review caveats. Include programmatic native import rows for config, instructions, skills, or supported plugins only if you personally migrated them in this follow-up run.
If only one scope has rows, render only the table with no heading. If multiple scopes have rows, render one heading before each table. Use
**User Config**for user-scope rows. For project-scope rows, use the actual project folder name as the heading, for example**northstar-support-portal**; do not useCurrent Projectas the heading. Do not add prose before or after the table output.Use exactly these columns:
northstar-support-portal
Status Item Notes AddedSlash commandpr-reviewConverted into a Codex skill AddedSubagentrelease-leadAdded as a Codex subagent Check before usingHookPreToolUseConverted, but some Claude hook behavior differs in Codex Not AddedHookNotificationCodex does not have an equivalent notification hook Not AddedPluginteam-macrosPlugin needs manual setup Statusmust beAdded,Check before using, orNot Added. UseAddedwhen a Codex-facing artifact was created or changed and needs no special review. UseCheck before usingwhen a Codex-facing artifact was created or changed but the migration changed semantics, inferred behavior, preserved tool rules as guidance, or dropped unsupported behavior. UseNot Addedwhen a source artifact was detected but no Codex-facing artifact was created.Itemcombines the artifact type and concrete item name in one cell. Artifact type must be singular:Skill,Slash command,Subagent,MCP,Hook, orPlugin. Wrap the artifact type in inline code; write the item name as plain text after it.Notesis always required; never leave it empty. Keep notes short, plain, and literal. Avoid internal implementation terms such as runtime expansion. Prefer phrases likeConverted into a Codex skill,Added as a Codex subagent,Added to Codex config,Converted into a Codex hook,Converted, but some Claude hook behavior differs in Codex,Codex does not have an equivalent notification hook,Plugin needs manual setup, orPlugin marketplace needs manual setup.
Self-Healing Loop
Keep looping until the selected migration is complete:
Run
--planor--doctor.Run the migration with
--dry-run.Run the migration for real.
Fix every generated
## MANUAL MIGRATION REQUIREDblock and everymanual_fix_requiredorskippedreport row that can be resolved inside Codex artifacts.Run
--validate-target.Re-run the migrator and validator until the report and validator have no actionable generated-artifact fixes left.
Do not edit source Claude Code files, unrelated project code, secrets, or another repository during this loop. If a report row requires source-provider changes or product judgment, leave the generated Codex artifact with clear manual guidance instead of changing the source.
Commands
Choose the migrator command.
MIGRATE_TO_CODEX='python3 .codex/skills/migrate-to-codex/scripts/migrate-to-codex.py'Inspect the migration before writing.
$MIGRATE_TO_CODEX --source ~/.claude/ --scan-only
$MIGRATE_TO_CODEX --source ~/.claude/ --target ~/.codex/ --plan
$MIGRATE_TO_CODEX --source ~/.claude/ --target ~/.codex/ --doctorDry-run, then run without --dry-run, for global and project.
$MIGRATE_TO_CODEX --source ~/.claude/ --target ~/.codex/ --dry-run
$MIGRATE_TO_CODEX --source ~/.claude/ --target ~/.codex/
$MIGRATE_TO_CODEX --source ./.claude/ --target ./.codex/ --dry-run
$MIGRATE_TO_CODEX --source ./.claude/ --target ./.codex/Run the post-migration validator against each target after edits.
$MIGRATE_TO_CODEX --validate-target ~/.codex/
$MIGRATE_TO_CODEX --validate-target ./.codex/Run $MIGRATE_TO_CODEX --help for flags (--scan-only, --plan, --doctor, --validate-target, defaults, and so on). Deep tables and more links are in references/differences.md.
Files
- ATTRIBUTION.mdAdded by AutoGineer890 B
- LICENSE.txt11.1 KiB
- SKILL.md7.8 KiB
- agents/openai.yaml144 B
- references/differences.md13.4 KiB
- scripts/cli.pyScript29.4 KiB
- scripts/migrate-to-codex.pyScript208 B
- scripts/migrate/__init__.pyScript285 B
- scripts/migrate/agents.pyScript9.8 KiB
- scripts/migrate/codex_config.pyScript3.9 KiB
- scripts/migrate/common.pyScript11.2 KiB
- scripts/migrate/hooks.pyScript8.0 KiB
- scripts/migrate/instructions.pyScript2.4 KiB
- scripts/migrate/mcps.pyScript6.9 KiB
- scripts/migrate/plugins.pyScript660 B
- scripts/migrate/settings.pyScript1.0 KiB
- scripts/migrate/skills.pyScript12.3 KiB
- scripts/utils/__init__.pyScript35 B
- scripts/utils/scan.pyScript4.3 KiB
- scripts/utils/util.pyScript9.2 KiB
scripts/migrate/common.py
"""Shared data models, frontmatter rendering, reporting, and path helpers.
Defines the artifact/report model used by every migration section, the
YAML-frontmatter adapter used for skills/agents/commands, Claude-model and
permission-mode partial mappings, and generic filesystem/report helpers. This
module should not know about one migration surface's control flow.
"""
from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field, fields as dataclass_fields
from enum import Enum
from pathlib import Path
from typing import TypeAlias
from migrate.settings import CLAUDE_SETTINGS_JSON_RELATIVE
from utils.util import (
format_yaml_mapping,
parse_yaml_mapping,
read_json_mapping_file,
)
FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\n?(.*)\Z", re.S)
CODEX_CONFIG_PATH = Path(".codex") / "config.toml"
CODEX_AGENTS_ROOT = Path(".codex") / "agents"
CODEX_SKILLS_ROOT = Path(".agents") / "skills"
SUMMARY_LABELS = {
"mcp_servers": "mcp servers",
}
PERMISSION_MODE_MAPPINGS = {
"acceptEdits": "workspace-write",
"readOnly": "read-only",
}
YamlScalar: TypeAlias = str | bool | int | float | None
YamlValue: TypeAlias = YamlScalar | Sequence[YamlScalar]
@dataclass(frozen=True)
class ScopePaths:
source: Path
is_global: bool
@dataclass(frozen=True)
class ModelMapping:
source_prefix: str
target_model: str
effort_mapping: tuple[tuple[str, str], ...]
def map_effort(self, effort: str) -> str:
for source_effort, target_effort in self.effort_mapping:
if effort == source_effort:
return target_effort
return effort
MODEL_PREFIX_MAPPINGS = (
ModelMapping(
"claude-opus",
"gpt-5.4",
(("low", "low"), ("medium", "medium"), ("high", "high"), ("max", "xhigh")),
),
ModelMapping(
"claude-sonnet",
"gpt-5.4-mini",
(("low", "medium"), ("medium", "high"), ("high", "xhigh"), ("max", "xhigh")),
),
ModelMapping(
"claude-haiku",
"gpt-5.4-mini",
(("low", "low"), ("medium", "medium"), ("high", "high"), ("max", "xhigh")),
),
)
class ArtifactKind(Enum):
FILE = "file"
SKILL = "skill"
AGENT = "agent"
@dataclass(frozen=True)
class GeneratedText:
content: str
@dataclass(frozen=True)
class SourceCopy:
source_path: Path
@dataclass(frozen=True)
class SourceSymlink:
source_path: Path
ArtifactPayload: TypeAlias = GeneratedText | SourceCopy | SourceSymlink
@dataclass(frozen=True)
class MigrationReportItem:
status: str
path: Path
detail: str
@dataclass(frozen=True)
class SimpleYamlFrontmatter:
values: dict[str, YamlValue]
def required_string(self, key: str) -> str:
return str(self.values[key])
def optional_string(self, key: str) -> str | None:
value = self.values.get(key)
if value is None:
return None
return str(value)
def string_tuple(self, key: str) -> tuple[str, ...]:
value = self.values.get(key)
if value is None:
return ()
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
return tuple(str(item).strip() for item in value if str(item).strip())
return tuple(
split_item
for split_item in (part.strip() for part in str(value).split(","))
if split_item
)
def to_dict(self) -> dict[str, YamlValue]:
return self.values
@dataclass(frozen=True)
class ParsedDocument:
frontmatter: SimpleYamlFrontmatter
body: str
path: Path | None = None
@classmethod
def from_file(cls, source_file: Path) -> ParsedDocument:
return parse_frontmatter(source_file.read_text(), source_file)
@dataclass(frozen=True)
class PlannedArtifact:
relative_path: Path
payload: ArtifactPayload
kind: ArtifactKind = ArtifactKind.FILE
@classmethod
def for_skill(cls, source_file: Path, content: str) -> PlannedArtifact:
return cls(
relative_path=CODEX_SKILLS_ROOT / source_file.parent.name / "SKILL.md",
payload=GeneratedText(content),
kind=ArtifactKind.SKILL,
)
@classmethod
def for_agent(cls, source_file: Path, content: str) -> PlannedArtifact:
return cls(
relative_path=CODEX_AGENTS_ROOT / f"{source_file.stem}.toml",
payload=GeneratedText(content),
kind=ArtifactKind.AGENT,
)
@classmethod
def from_source_file(
cls, source_file: Path, relative_path: Path
) -> PlannedArtifact:
return cls(
relative_path=relative_path,
payload=SourceCopy(source_file),
)
def prefixed(self, prefix: Path) -> PlannedArtifact:
return PlannedArtifact(
relative_path=prefix / self.relative_path,
payload=self.payload,
kind=self.kind,
)
def without_prefix(self) -> PlannedArtifact:
return PlannedArtifact(
relative_path=Path(*self.relative_path.parts[1:]),
payload=self.payload,
kind=self.kind,
)
@dataclass
class MigrationSummary:
instructions: int = 0
skills: int = 0
subagents: int = 0
mcp_servers: int = 0
orphaned_skills: int = 0
orphaned_subagents: int = 0
def add(self, other: MigrationSummary) -> None:
for summary_field in dataclass_fields(self):
field_name = summary_field.name
setattr(
self,
field_name,
getattr(self, field_name) + getattr(other, field_name),
)
def render(self, deploy_mode: object, dry_run: bool) -> str:
suffix = " (dry-run)" if dry_run else ""
deploy_mode_value = getattr(deploy_mode, "value", str(deploy_mode))
lines = [
f"Migration summary{suffix}:",
f" deploy mode: {deploy_mode_value}",
]
for summary_field in dataclass_fields(self):
field_name = summary_field.name
value = getattr(self, field_name)
label = SUMMARY_LABELS.get(field_name, field_name.replace("_", " "))
lines.append(f" {label}: {value}")
return "\n".join(lines)
@dataclass
class ConversionResult:
summary: MigrationSummary = field(default_factory=MigrationSummary)
artifacts: list[PlannedArtifact] = field(default_factory=list)
report_items: list[MigrationReportItem] = field(default_factory=list)
def add(self, other: ConversionResult) -> None:
self.summary.add(other.summary)
self.artifacts.extend(other.artifacts)
self.report_items.extend(other.report_items)
def prefixed(self, prefix: Path) -> ConversionResult:
return ConversionResult(
summary=self.summary,
artifacts=[artifact.prefixed(prefix) for artifact in self.artifacts],
report_items=[
MigrationReportItem(
item.status,
prefix / item.path,
item.detail,
)
for item in self.report_items
],
)
def json_object(value: object) -> Mapping[str, object]:
if isinstance(value, Mapping):
return value
return {}
def json_string(value: object) -> str | None:
if value is None:
return None
return str(value)
def json_string_tuple(value: object) -> tuple[str, ...]:
if value is None:
return ()
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
return tuple(str(item) for item in value)
return (str(value),)
def load_scope_settings(scope_root: Path) -> Mapping[str, object]:
settings: dict[str, object] = {}
for rel in CLAUDE_SETTINGS_JSON_RELATIVE:
outcome = read_json_mapping_file(scope_root / rel)
if outcome.exists and outcome.ok:
settings.update(json_object(outcome.data))
return settings
def format_bullets(values: Sequence[str], prefix: str = "") -> str:
return "\n".join(f"- {prefix}{value}" for value in values)
def format_manual_migration_block(notes: Sequence[str]) -> str:
return "## MANUAL MIGRATION REQUIRED\n\n" + "\n\n".join(
note.rstrip() for note in notes if note.strip()
)
def unsupported_frontmatter_fields(
frontmatter_values: Mapping[str, YamlValue],
supported_fields: Sequence[str],
) -> tuple[str, ...]:
supported = frozenset(supported_fields)
return tuple(
sorted(
field_name
for field_name in frontmatter_values
if field_name not in supported
)
)
def append_report_item(
report_items: list[MigrationReportItem],
requires_manual_fix: object,
path: Path,
manual_detail: str,
rewritten_detail: str,
) -> None:
if requires_manual_fix:
report_items.append(manual_report_item(path, manual_detail))
return
report_items.append(MigrationReportItem("rewritten", path, rewritten_detail))
def manual_report_item(path: Path, detail: str) -> MigrationReportItem:
return MigrationReportItem("manual_fix_required", path, detail)
def report_manual_paths(
scope: ScopePaths,
path_labels: Sequence[tuple[Path, str]],
) -> ConversionResult:
result = ConversionResult()
for relative_path, label in path_labels:
if path_exists_with_exact_case(scope.source / relative_path):
result.report_items.append(
manual_report_item(
relative_path,
f"Manual review required for {label}; not converted by this tool.",
)
)
return result
def path_exists_with_exact_case(path: Path) -> bool:
if not path.exists():
return False
try:
return path.name in {child.name for child in path.parent.iterdir()}
except FileNotFoundError:
return False
def is_path_within_root(path: Path, root: Path) -> bool:
try:
path.resolve().relative_to(root.resolve())
except ValueError:
return False
return True
def parse_frontmatter(content: str, path: Path | None = None) -> ParsedDocument:
match = FRONTMATTER_RE.match(content)
if not match:
return ParsedDocument(SimpleYamlFrontmatter({}), content, path)
raw_frontmatter, body = match.groups()
return ParsedDocument(parse_yaml_frontmatter(raw_frontmatter, path), body, path)
def parse_yaml_frontmatter(
content: str,
path: Path | None = None,
) -> SimpleYamlFrontmatter:
return SimpleYamlFrontmatter(parse_yaml_mapping(content))
def format_frontmatter(frontmatter: SimpleYamlFrontmatter, body: str) -> str:
rendered = format_yaml_mapping(frontmatter.to_dict())
return f"---\n{rendered}\n---\n\n{body.lstrip()}"
def map_model_name(model: str) -> str:
for mapping in MODEL_PREFIX_MAPPINGS:
if model.startswith(mapping.source_prefix):
return mapping.target_model
return model
def map_model_effort(model: str | None, effort: str) -> str:
if not model:
return effort
for mapping in MODEL_PREFIX_MAPPINGS:
if model.startswith(mapping.source_prefix):
return mapping.map_effort(effort)
return effort
def map_permission_mode(permission_mode: str | None) -> str | None:
if not permission_mode:
return None
return PERMISSION_MODE_MAPPINGS.get(permission_mode)
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 an MCP server, 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/.curated/migrate-to-codex · 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 migrate-to-codex.zip