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

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

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/external-agent-migration
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

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

在 TUI 里输入 `/import`，Codex 会同时检查 Claude Code 和 Cursor 留下的配置，让你挑一个来源、勾选要导入的项；原来的文件只读不改。能导入什么、导入后要检查什么，见手册的[从其他 agent 导入](https://daiw.net/manual/codex/cloud-integrations)。这一篇看它怎么实现。

整件事分三层：`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` | 同名跳过 |
| `Hooks` | `hooks.json`，并复制 hook 脚本 | 目标为空才写 |
| `Plugins` | 经插件管理器安装 | 只允许家目录范围 |
| `Memory` | `~/.codex/memories/extensions/external_agent_import/resources/` | 仅 `Cla`，且需开功能开关 |
| `Sessions` | 新建的 Codex 线程 | 靠导入账本去重 |

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

```rust
    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 从环境变量取值的字段：

```rust
    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` 则这样映射：

```rust
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`）

<Callout type="warn">
  迁移器会把 `permissionMode` 写成 agent 文件里的 `sandbox_mode`，但[多 agent](https://daiw.net/manual/codex-source/multi-agent) 那篇讲过，v0.158.0 的角色层只认一张白名单，`sandbox_mode` 不在其中，子代理的沙箱仍跟父轮次走。也就是说这个字段导进来了，运行时不生效。
</Callout>

**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`（整合过程见[上一篇](https://daiw.net/manual/codex-source/memories)）。

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

会话只在家目录范围检测，默认取最近 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。源文件没变就不再列出；导入后源会话又长出新内容，则追加到原来那条线程上，而不是再建一条。

## 执行与报告

```mermaid
sequenceDiagram
  participant T as TUI /import
  participant A as app-server
  participant S as ExternalAgentConfigService
  T->>A: externalAgentConfig/detect 两个来源各一次
  A->>S: 检测家目录与当前仓库
  S-->>A: 只返回会带来变化的迁移项
  T->>T: 选择来源 勾选迁移项
  T->>A: externalAgentConfig/import
  A->>S: 逐项导入 只补不覆盖
  A-->>T: 返回 importId 再逐项发 progress
  A->>A: 后台导入会话与远程插件
  A-->>T: externalAgentConfig/import/completed
```

`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](https://daiw.net/manual/llm-to-agent/mcp) 与 [Hooks](https://daiw.net/manual/llm-to-agent/hooks) 讲的都是“一个 agent 怎么接外部能力”；迁移器面对的是另一个问题：两个 agent 的扩展格式大同小异，怎么把用户在别处攒下的东西搬过来。Codex 的答案是保守的三条：只搬能确定语义的字段，冲突时永远保留用户已有的配置，语义拿不准的内容（比如映射不成环境变量的 MCP 占位符、异步 hook）宁可不搬。

---

上一篇：[记忆 · 从历史会话里提炼经验](https://daiw.net/manual/codex-source/memories) · 下一篇：[TUI 架构 · 事件、线程路由与渲染](https://daiw.net/manual/codex-source/tui-architecture)
