从别的 agent 迁移 · 导入 Claude Code 与 Cursor 的配置

/import 背后是 codex-external-agent-migration:代码里把 Claude Code 叫 cla、Cursor 叫 cur,检测时先在内存里合并一遍、只列出真能带来新东西的项,导入时一律只补不覆盖,并把 CLAUDE.md、设置、MCP、hooks、子代理、命令、会话改写成 Codex 的格式;app-server 负责逐项报告进度,会话与远程插件在后台导入。

作者 David更新于 第 43 篇(共 57 篇)

从别的 agent 迁移 · 导入 Claude Code 与 Cursor 的配置

在 TUI 里输入 /import,Codex 会同时检查 Claude Code 和 Cursor 留下的配置,让你挑一个来源、勾选要导入的项;原来的文件只读不改。能导入什么、导入后要检查什么,见手册的从其他 agent 导入。这一篇看它怎么实现。

整件事分三层:codex-external-agent-migration crate 只管文件层面的“检测”和“导入”;app-server 的 external_agent_migration 模块把它包成 JSON-RPC 方法,管进度、后台任务和历史记录;TUI 的 external_agent_config_migration 负责选来源、勾选和展示结果。

cla 与 cur

源码里两个来源一律用缩写:枚举 ExternalAgentSource 只有 Cla 和 Cur 两个成员,对应 ClaSource 与 CurSource,文件名也是 source_cla.rs、hooks_cur.rs、records_cla.rs 这样成对出现。TUI 把它们显示为 “Claude Code” 和 “Cursor”,请求参数 migrationSource 分别填 claude-code 和 cursor;服务端只认 cursor(不区分大小写)为 Cur,其余一律按默认的 Cla 处理。

Cla(Claude Code)Cur(Cursor)
配置目录.claude(家目录或仓库里).cursor
设置文件settings.json,再叠上同目录的 settings.local.json家目录 cli-config.json,仓库 cli.json
指令文件仓库根或 .claude/ 下的 CLAUDE.md,家目录 CLAUDE.md仓库根的 .cursorrules
MCP.mcp.json、.claude.json,以及 ~/.claude.json 里本项目的条目.cursor/mcp.json
会话~/.claude/projects/ 下的 *.jsonl~/.cursor/projects/ 下各项目的 agent-transcripts

十类迁移项与冲突规则

ExternalAgentConfigMigrationItemType 有十个成员。每一项都按“范围”处理:MigrationScope 要么是家目录,要么是一个仓库(从 cwd 往上找 .git,找不到就用 cwd 本身)。

迁移项写到哪里目标已存在时
Config~/.codex/config.toml 或仓库 .codex/config.toml只补缺失的键
AgentsMd~/.codex/AGENTS.md 或仓库根 AGENTS.md目标为空才写
Skills~/.agents/skills 或仓库 .agents/skills同名目录跳过
Commands同上,变成名为 source-command-<名字> 的 skill同名跳过
McpServerConfig同一个 config.toml 的 [mcp_servers]同名服务器跳过
Subagents~/.codex/agents 或仓库 .codex/agents 下的 <名字>.toml同名跳过
Hookshooks.json,并复制 hook 脚本目标为空才写
Plugins经插件管理器安装只允许家目录范围
Memory~/.codex/memories/extensions/external_agent_import/resources/仅 Cla,且需开功能开关
Sessions新建的 Codex 线程靠导入账本去重

“只补不覆盖”落在一个递归合并函数上:键不存在才插入,两边都是表才往下走,其余一律保留原值:

    match (existing, incoming) {
        (TomlValue::Table(existing_table), TomlValue::Table(incoming_table)) => {
            let mut changed = false;
            for (key, incoming_value) in incoming_table {
                match existing_table.get_mut(key) {
                    Some(existing_value) => {
                        if matches!(
                            (&*existing_value, incoming_value),
                            (TomlValue::Table(_), TomlValue::Table(_))
                        ) && merge_missing_toml_values(existing_value, incoming_value)?
                        {
                            changed = true;
                        }
                    }
                    None => {
                        existing_table.insert(key.clone(), incoming_value.clone());
                        changed = true;
                    }
                }
            }
            Ok(changed)

(codex-rs/external-agent-migration/src/config_values.rs:12)

检测复用同一套逻辑。ExternalAgentConfigService::detect 对每一项都先算出迁移结果,在内存里对目标做一次合并或比对,只有返回“会有变化”的项才列出来,所以列表里不会出现导了也没用的东西。另有两道安全检查:仓库范围下,.codex、.codex/config.toml、.codex/agents、.codex/hooks、.agents、.agents/skills 任何一个是符号链接(Windows 上还包括重解析点),整个仓库范围直接放弃;插件安装是全局的,仓库范围的插件迁移会报 repository-scoped plugin migration is not allowed。

改写:换名字,也换格式

换名字靠 RewriteProfile。Cla 把 CLAUDE.md 换成 AGENTS.md,把 claude code、claude-code、claude_code、claudecode、claude 换成 Codex(不区分大小写、按词边界);Cur 把 .cursorrules 换成 AGENTS.md,把区分大小写的 Cursor 换成 Codex。它作用在 AGENTS.md 正文、每个 skill 的 SKILL.md、子代理的描述与正文、hook 的状态提示上。

设置只迁很少的东西:env 变成 shell_environment_policy(inherit = "core" 加一张 set 表);Cla 的 sandbox.enabled = true 变成 sandbox_mode = "workspace-write"。

MCP 服务器按传输分两种:带 command 的 stdio 服务器取 command、args、env,带 url 的只接受 http 或 streamable_http(其它类型如 sse 直接跳过),被 enabledMcpjsonServers、disabledMcpjsonServers 或 disabled 排除的也跳过。最讲究的是 ${VAR} 占位符,它们不能原样抄进 Codex 的配置,于是改写成 Codex 从环境变量取值的字段:

    for (key, value) in headers {
        let header_value = json_string(value).unwrap_or_else(|| value.to_string());
        if key.eq_ignore_ascii_case("authorization")
            && let Some(token_env) = header_value
                .strip_prefix("Bearer ")
                .and_then(parse_env_placeholder)
        {
            table.insert(
                "bearer_token_env_var".to_string(),
                TomlValue::String(token_env),
            );
            continue;
        }

        if let Some(env_var) = parse_env_placeholder(&header_value) {
            env_headers.insert(key.clone(), TomlValue::String(env_var));
        } else if contains_env_placeholder(&header_value) {
            return None;
        } else {
            static_headers.insert(key.clone(), TomlValue::String(header_value));
        }
    }

(codex-rs/external-agent-migration/src/mcp.rs:271)

Authorization: Bearer ${TOKEN} 变成 bearer_token_env_var,整值是占位符的头进 env_http_headers,普通头进 http_headers,混着占位符的就放弃整个服务器。env 同理:值恰好是同名 ${KEY} 的进 env_vars,其它带占位符的也让整个服务器作罢。宁可少导,不导一份跑不起来的配置。

子代理从 agents/*.md 的 frontmatter 转成 TOML:必须有 name、description 和非空正文,正文进 developer_instructions;effort 映射成 model_reasoning_effort(max 改叫 xhigh,不认识的值丢弃),permissionMode 则这样映射:

fn map_agent_permission_mode(permission_mode: &str) -> Option<&'static str> {
    match permission_mode {
        "acceptEdits" => Some("workspace-write"),
        "readOnly" => Some("read-only"),
        _ => None,
    }
}

(codex-rs/external-agent-migration/src/subagents.rs:296)

迁移器会把 permissionMode 写成 agent 文件里的 sandbox_mode,但多 agent 那篇讲过,v0.158.0 的角色层只认一张白名单,sandbox_mode 不在其中,子代理的沙箱仍跟父轮次走。也就是说这个字段导进来了,运行时不生效。

hooks 只迁能对上的部分。Cla 读 settings.json 与 settings.local.json,disableAllHooks 为真就什么都不迁;事件名按 Codex 的 12 个事件逐个取;一组 hook 只能有 matcher 和 hooks 两个键,里面只收 type 为 command、没有 async、shell、once、asyncRewake 等额外能力的命令;命令里指向 .claude/hooks/ 的路径改写到新位置,脚本一并复制。Cur 从 hooks.json 读,并把事件名映射过来,例如 beforeSubmitPrompt 对应 UserPromptSubmit。

项目记忆只有 Cla 有,还要打开开发中的功能开关 external_agent_memory_import:~/.claude/projects/<项目>/memory/ 下的 Markdown 原样复制到上表的 resources/<项目>/,旁边写一份记录该项目 cwd 的 scope.json,再给这个记忆扩展写一份说明,最后排队一次记忆的 Phase 2 整合,由整合子代理把它们并进 MEMORY.md(整合过程见上一篇)。

会话:变成只有文字的对话记录

会话只在家目录范围检测,默认取最近 30 天、最多 50 个(externalAgentConfig/detect 可以用 maxSessionAgeDays、maxSessions 调)。导入时逐条读 JSONL 记录,只留用户与助手消息:thinking 丢掉,tool_use 写成 [external_agent_tool_call: 工具名] 开头的文字注记(带上描述、命令或文件路径),tool_result 截到 4000 个字符。每条用户消息开一个合成的轮次(external-import-turn-N),最后追加一条 <EXTERNAL SESSION IMPORTED> 标记和按字节数估算的 token 用量。导入后它是一条普通线程,memory_mode 跟随 memories.generate_memories,因此也在记忆管线的候选范围内(仍要满足那边的时间窗口)。

去重靠 ~/.codex/external_agent_session_imports.json 这本账:记下源文件路径、内容的 SHA-256 和导入后的线程 id。源文件没变就不再列出;导入后源会话又长出新内容,则追加到原来那条线程上,而不是再建一条。

执行与报告

图表加载中…

ExternalAgentConfigService::import 逐项执行,每项产出一个 ExternalAgentConfigImportItemResult:成功数、失败数、每个成功条目的来源与去向、每个错误的阶段与消息。一项失败不影响其它项;目标 config.toml 本身解析不了时,错误类型标为 invalid_existing_config。app-server 在同步部分做完后先回 importId,再为每项发一条 externalAgentConfig/import/progress;会话(并发 5 个)和来自远程 marketplace 的插件放到后台,全部结束后发 externalAgentConfig/import/completed,同时写进状态库的导入历史,供 externalAgentConfig/import/readHistories 读取。只要选了设置、skills、MCP、hooks、命令或插件中的任何一项,同步部分结束后都会触发一次运行时配置刷新;后台装完插件后,还会清空插件与 skills 的缓存。

和《从 LLM 到 Coding Agent》对照

那本的 MCP 与 Hooks 讲的都是“一个 agent 怎么接外部能力”;迁移器面对的是另一个问题:两个 agent 的扩展格式大同小异,怎么把用户在别处攒下的东西搬过来。Codex 的答案是保守的三条:只搬能确定语义的字段,冲突时永远保留用户已有的配置,语义拿不准的内容(比如映射不成环境变量的 MCP 占位符、异步 hook)宁可不搬。


上一篇:记忆 · 从历史会话里提炼经验 · 下一篇:TUI 架构 · 事件、线程路由与渲染

本页目录