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/utils/util.py
from __future__ import annotations
import glob
import json
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import TypeAlias
YamlScalar: TypeAlias = str | bool | int | float | None
YamlValue: TypeAlias = YamlScalar | Sequence[YamlScalar]
TomlScalar: TypeAlias = str | bool | int | float | None
TomlValue: TypeAlias = object
def detected_json_keys(content: str, keys: Sequence[str]) -> tuple[str, ...]:
return tuple(key for key in keys if re.search(rf'"{re.escape(key)}"\s*:', content))
def strip_jsonc_comments(content: str) -> str:
lines: list[str] = []
for line in content.splitlines():
in_string = False
escaped = False
result: list[str] = []
index = 0
while index < len(line):
char = line[index]
if escaped:
result.append(char)
escaped = False
index += 1
continue
if char == "\\" and in_string:
result.append(char)
escaped = True
index += 1
continue
if char == '"':
in_string = not in_string
result.append(char)
index += 1
continue
if (
not in_string
and char == "/"
and index + 1 < len(line)
and line[index + 1] == "/"
):
break
result.append(char)
index += 1
lines.append("".join(result))
return "\n".join(lines)
def load_jsonc_object(content: str, json_object: callable) -> Mapping[str, object]:
without_comments = strip_jsonc_comments(content)
without_trailing_commas = re.sub(r",\s*([}\]])", r"\1", without_comments)
return json_object(json.loads(without_trailing_commas))
def parse_jsonc_mapping_text(text: str) -> Mapping[str, object] | None:
"""Return the top-level JSON object, or None if the text is not a JSON object."""
try:
without_comments = strip_jsonc_comments(text)
without_trailing_commas = re.sub(r",\s*([}\]])", r"\1", without_comments)
parsed = json.loads(without_trailing_commas)
except (json.JSONDecodeError, TypeError, ValueError):
return None
if isinstance(parsed, Mapping):
return parsed
return None
@dataclass(frozen=True)
class JsonMappingFileRead:
exists: bool
ok: bool
data: Mapping[str, object]
def read_json_mapping_file(path: Path) -> JsonMappingFileRead:
"""Read a JSON/JSONC file. ``ok`` is False when the file exists but could not be parsed."""
if not path.is_file():
return JsonMappingFileRead(exists=False, ok=True, data={})
text = path.read_text()
parsed = parse_jsonc_mapping_text(text)
if parsed is None:
return JsonMappingFileRead(exists=True, ok=False, data={})
return JsonMappingFileRead(exists=True, ok=True, data=parsed)
@dataclass(frozen=True)
class TomlMultilineString:
value: str
def parse_yaml_mapping(content: str) -> dict[str, YamlValue]:
"""Parse the small YAML-frontmatter subset used by Claude metadata."""
result: dict[str, YamlValue] = {}
current_key: str | None = None
for raw_line in content.splitlines():
if not raw_line.strip():
continue
if raw_line.startswith(" - ") and current_key:
current_value = result.setdefault(current_key, [])
if not isinstance(current_value, list):
current_value = [current_value]
result[current_key] = current_value
current_value.append(parse_yaml_value(raw_line[4:].strip()))
continue
key, separator, value = raw_line.partition(":")
if not separator:
continue
current_key = key.strip()
value = value.strip()
result[current_key] = parse_yaml_value(value) if value else []
return result
def parse_yaml_value(value: str) -> YamlValue:
if value in {"true", "True"}:
return True
if value in {"false", "False"}:
return False
if value in {"null", "Null", "~"}:
return None
if value.startswith("[") and value.endswith("]"):
return tuple(
parse_yaml_value(item)
for item in split_delimited_values(value[1:-1])
if item
)
if value.startswith('"') and value.endswith('"'):
try:
return json.loads(value)
except json.JSONDecodeError:
return value[1:-1]
if value.startswith("'") and value.endswith("'"):
return value[1:-1].replace("''", "'")
return value
def split_delimited_values(content: str) -> tuple[str, ...]:
values: list[str] = []
token: list[str] = []
quote: str | None = None
escaped = False
for char in content:
if escaped:
token.append(char)
escaped = False
continue
if char == "\\" and quote == '"':
token.append(char)
escaped = True
continue
if quote:
token.append(char)
if char == quote:
quote = None
continue
if char in {"'", '"'}:
token.append(char)
quote = char
continue
if char == ",":
values.append("".join(token).strip())
token = []
continue
token.append(char)
values.append("".join(token).strip())
return tuple(values)
def format_yaml_mapping(values: Mapping[str, YamlValue]) -> str:
return "\n".join(
f"{key}: {format_yaml_value(value)}" for key, value in values.items()
)
def format_yaml_value(value: YamlValue) -> str:
if isinstance(value, bool):
return "true" if value else "false"
if value is None:
return "null"
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
return "[" + ", ".join(format_yaml_value(item) for item in value) + "]"
return json.dumps(str(value))
def render_toml_document(values: Mapping[str, TomlValue]) -> str:
lines: list[str] = []
append_toml_entries(lines, values)
for key, value in values.items():
if isinstance(value, Mapping):
append_toml_table(lines, (key,), value)
return "\n".join(lines).rstrip() + "\n"
def append_toml_table(
lines: list[str],
path: tuple[str, ...],
values: Mapping[str, TomlValue],
) -> None:
append_blank_line(lines)
lines.append("[" + ".".join(format_toml_key(path_part) for path_part in path) + "]")
append_toml_entries(lines, values)
for key, value in values.items():
if isinstance(value, Mapping):
append_toml_table(lines, (*path, key), value)
def append_toml_entries(lines: list[str], values: Mapping[str, TomlValue]) -> None:
for key, value in values.items():
if isinstance(value, Mapping):
continue
lines.append(f"{format_toml_key(key)} = {format_toml_value(value)}")
def append_blank_line(lines: list[str]) -> None:
if lines and lines[-1]:
lines.append("")
def format_toml_key(key: str) -> str:
if re.fullmatch(r"[A-Za-z0-9_-]+", key):
return key
return json.dumps(key)
def format_toml_value(value: TomlValue) -> str:
if isinstance(value, TomlMultilineString):
return format_toml_multiline_string(value.value)
if isinstance(value, bool):
return "true" if value else "false"
if value is None:
return '""'
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
return "[" + ", ".join(format_toml_value(item) for item in value) + "]"
return json.dumps(str(value))
def format_toml_multiline_string(value: str) -> str:
escaped = value.replace("\\", "\\\\").replace('"""', '\\"\\"\\"')
return f'"""{escaped}"""'
def slugify_name(value: str) -> str:
result = re.sub(r"[^A-Za-z0-9_-]+", "-", value.strip()).strip("-").lower()
return result or "migrated-command"
def first_markdown_heading(content: str) -> str | None:
for line in content.splitlines():
match = re.match(r"^#\s+(.+?)\s*$", line)
if match:
return match.group(1).strip()
return None
def format_backtick_list(values: Sequence[str]) -> str:
if not values:
return ""
if len(values) == 1:
return f"`{values[0]}`"
return ", ".join(f"`{value}`" for value in values[:-1]) + f", and `{values[-1]}`"
def normalize_source_scope_root(
path: Path, source_scope_markers: Sequence[Path]
) -> Path:
resolved = path
for marker in source_scope_markers:
if resolved.parts[-len(marker.parts) :] == marker.parts:
return resolved.parents[len(marker.parts) - 1]
return resolved
def resolve_source_root(source: str) -> Path:
if not glob.has_magic(source):
return Path(source)
matches = [Path(match) for match in glob.glob(source, recursive=True)]
if not matches:
raise FileNotFoundError(f"No matches for source pattern: {source}")
for match in matches:
if (
match.is_dir()
and (match / "global").exists()
and (match / "project").exists()
):
return match
static_prefix = source.split("*", 1)[0].rstrip("/")
return Path(static_prefix)
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