你有没有想过一个问题:用 Claude Code 或者 Codex 聊了那么多,关掉终端、电脑重启,第二天 claude --resume 又能把之前的对话拉回来,这些记录到底存在哪?换了 API 网关、登录了别的账号,甚至给工具换了个 provider,它们会不会就没了?
这篇就把这件事讲清楚:聊天记录存在哪、为什么换来换去一般都不影响它、什么情况下才真的会「丢」。
在看具体存储位置之前,先认识一个文件格式:jsonl。
普通的 JSON 文件是「整个文件凑成一个 JSON 对象」,比如要存两条用户数据,得用一个数组把它们裹在一起,元素之间还要用逗号隔开:
[
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]而 jsonl(JSON Lines)是「一行一个独立的 JSON 对象」,没有外层的数组,行与行之间也不用逗号,每一行单拎出来都是一个完整的 JSON:
{"name": "Alice", "age": 30}
{"name": "Bob", "age": 25}聊天记录天然适合后面这种格式:每说一句话就往文件末尾追加一行。
如果用标准 json 格式,每次都得把整个文件加载到内存才能添加内容,效率很低。jsonl 直接在文件末尾追加一行就行,效率高很多。
Claude Code 和 Codex 的聊天记录都是用 jsonl 文件存储的,下面来看它们具体的存储位置和存储格式。
Claude Code 的聊天记录
Claude Code 把每一次会话都存成你电脑上的一个文件,位置在:
~/.claude/projects/<项目路径>/<会话 ID>.jsonl它按「项目」分文件夹,每个项目一个目录,目录里每个会话一个 .jsonl 文件。这里的 <项目路径> 不是真的带斜杠的路径,而是把项目绝对路径里的 / 之类字符全换成 -。比如你在 /Users/alice/code/myapp 这个项目里聊天,对应的目录就是:
~/.claude/projects/-Users-alice-code-myapp/这套「项目路径转码」的规则,和 Claude Code 的常用技巧 里讲的 auto memory 目录是同一套。
打开任意一个 .jsonl 文件,你会看到一行一条消息,你问的、模型答的、中间调了什么工具,都按时间顺序记在里面。精简一下大概长这样:
{"type":"user","sessionId":"db4961c3-...","uuid":"87bb5552-...","parentUuid":null,"message":{"role":"user","content":"帮我写一个二分查找"}}
{"type":"assistant","sessionId":"db4961c3-...","uuid":"a1b2c3d4-...","parentUuid":"87bb5552-...","message":{"role":"assistant","content":"好的,二分查找的思路是..."}}每行都带个 sessionId,标明这条消息属于哪次会话,同一个文件里所有行的 sessionId 都一样,就是文件名里那个会话 ID。
每条消息有自己的 uuid,再用 parentUuid 指向它的上一条。第一条 user 消息没有上文,parentUuid 是 null;第二条 assistant 消息的 parentUuid 正好是第一条的 uuid,一条接一条,整段对话就被串成了一条链。
所谓 claude --resume 恢复会话,本质就是把这个文件重新读出来,顺着这条链还原成完整对话。
这里再次强调 上下文和 token 到底是什么 中讲过的关键认知:模型本身是无状态的,它那边不保存你的任何对话,根本不记得你。每次你发消息,其实是把这份本地记录里的历史整段打包,全量发给模型。
想清楚这件事,就知道为什么换接口、换账号、换模型都不影响记录了:记录是你本地的文件,你换接口、换登录账号、换模型,变的只是「找谁来算」,本地这份 .jsonl 一个字都没动。
另外,除了主会话(和你直接交互的会话)外,Claude Code 还可能创建 Sub Agent,在全新的上下文中执行一些任务。关于 Sub Agent 的原理会在 Sub Agent:上下文管理的艺术 详细介绍,这里只介绍会话记录的存储路径。
这些子 agent 的对话不会混进主会话文件,而是单独存放。每个主会话文件旁边,有一个和会话 ID 同名的子目录,子 agent 的历史就在里面的 subagents/ 文件夹下:
~/.claude/projects/<项目路径>/
├── 9f558c28-....jsonl # 主会话
└── 9f558c28-.../ # 和会话 ID 同名的子目录
└── subagents/
├── agent-a980c047....jsonl # 一个子 agent 的完整对话
└── agent-a980c047....meta.json # 它的元数据(agent 类型、任务描述)每个子 agent 一个 .jsonl,结构和主会话一样也是一行一条消息,外加一个 .meta.json 记着子 agent 的类型和被派去干的活。主会话里只留一条「调用了某个子 agent」的记录,子 agent 内部的所有上下文和对话记录都放在独立的文件里。
Codex 的聊天记录
Codex 也把记录存在本地,只是组织方式和 Claude Code 不太一样。它不按项目分,而是按日期分:
~/.codex/sessions/2026/06/03/rollout-2026-06-03T09-22-56-<会话 ID>.jsonl同样是 jsonl,但 Codex 每个会话文件的第一行是一段元数据(叫 session_meta),记着这次会话的基本信息,从第二行起才是真正的对话内容:
{"type":"session_meta","payload":{"id":"019e8b13-...","cwd":"/Users/me/proj","model_provider":"openai"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"帮我写一个二分查找"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"好的,二分查找的思路是..."}]}}对话行同样带 role 和 content,但比 Claude Code 还简单,没有 uuid、parentUuid 那一套,对话顺序就是文件里行的先后顺序,读出来按顺序还原就行。
对于 sub agent 的对话历史,Codex 的处理思路和 Claude Code 不太一样。Claude Code 把子 agent 收进主会话的子目录里,而 Codex 则把每个子 agent 也当成一个独立的会话文件,平级地混在同一个日期目录下,不单独建文件夹。
Codex 靠文件头 session_meta 里的 source 字段区分文件来源:命令行的 Codex CLI 会话记录为 cli,子 agent 记录成 subagent,连父会话 ID 也一并记在这个字段里。所以光看目录你分不出主次,得读取文件头的 session_meta 才能理清主会话和 sub agent 的调用关系。
不过使用 Codex 时有一个容易踩的问题值得说一下。
聊天记录丢了?
肯定有读者遇到过:切换 Codex 的 provider 使用第三方的 API 接口之后,聊天记录全没了。
这是怎么回事呢?注意 jsonl 文件里那个 model_provider 字段,它记着这次会话当时用的是哪个 provider,问题就出在这。
比如你使用中转站时,可能在 ~/.codex/config.toml 有如下配置:
model_provider = "my-provider"
[model_providers.my-provider]
name = "my-provider"
base_url = ...Codex 在读取历史会话列表的时候,默认只筛选当前 provider 的会话。
也就是说,如果你原来用 OpenAI 官方(model_provider 是 openai),后来换成某个第三方 provider(比如命名为 my-provider),再查看历史会话列表,会发现之前的对话全不见了,像是被清空了一样。
但它们其实一个都没丢,文件还安安静静躺在 ~/.codex/sessions/ 下,只是 Codex 拿当前 provider 去跟每个会话文件头里的 model_provider 比对,对不上的就不显示。
你只要把 provider 切回原来的 openai,列表里立刻又全回来了。这不是聊天记录丢了,而是被 Codex 藏起来了。
恢复聊天记录
知道了原理,就能想办法解决了。
如果你一直在用第三方的 provider,最简单的办法是:切换 provider 的时候只修改 base_url 等配置,不要动 provider name:
model_provider = "my-provider"
[model_providers.my-provider]
name = "my-provider"
# 不要修改 provider name
# 只修改下面的具体配置
base_url = ...但如果你之前用的是 OpenAI 官方订阅,官方的 provider name 是 openai,这个值没法在配置文件中手动设置。
最彻底的通用解决方案是:把所有会话文件头里的 model_provider 字段批量改成新值。既然 Codex 按这个字段过滤,那把所有会话历史的 model_provider 都改为当前值,自然就能在新 provider 下看到所有会话历史了。
下面这个 Python 脚本就干这件事,遍历所有会话文件,只动第一行那段元数据里的 model_provider,正文一个字不碰:
import json
import os
import sys
from pathlib import Path
# 把 Codex 会话记录里的 model_provider 从旧值批量改成新值
# 用法:python flip.py <旧 provider> <新 provider> [--apply]
old, new = sys.argv[1], sys.argv[2]
apply = "--apply" in sys.argv
codex_home = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
hit = 0
for f in (codex_home / "sessions").rglob("rollout-*.jsonl"):
text = f.read_text(encoding="utf-8")
# 只动第一行的 session_meta,正文原样保留
head, sep, rest = text.partition("\n")
meta = json.loads(head)
payload = meta.get("payload", meta)
if payload.get("model_provider") != old:
continue
hit += 1
if apply:
payload["model_provider"] = new
f.write_text(json.dumps(meta, ensure_ascii=False) + sep + rest, encoding="utf-8")
verb = "已改写" if apply else "匹配到"
print(f"{verb} {hit} 个会话")
if not apply:
print("确认无误后加 --apply 真正执行")先不加 --apply 跑一遍,它只统计、不改任何东西,让你确认会动多少个会话:
# 假设原来用 openai,现在换到了名为 my-provider 的第三方
uv run flip.py openai my-provider
# 输出:匹配到 101 个会话数字对得上,再加 --apply 真正执行:
uv run flip.py openai my-provider --apply
# 输出:已改写 101 个会话改完后重启 Codex,之前的聊天记录就都回来了。这段脚本我在自己机器上真跑过,所有会话全部改写成功,对话正文没有任何损坏。
补充一个细节:较新版本的 Codex 会把很久没碰过的冷会话压缩成 .jsonl.zst 结尾的文件,上面脚本只匹配 .jsonl,碰到压缩过的会话会跳过。如果你发现改完还有少量记录没回来,多半是它们已经被压缩了,先解压再跑一遍即可。
总结
说到底,AI 工具的聊天记录都是你本地的文件,模型只是个无状态的「计算器」,换 provider、换账号都改不了这些文件本身。
如果你看到「记录没了」,多半只是某个列表的过滤规则把它藏起来了,翻翻对应目录就能确认它还在。
真正会让记录彻底消失的只有两种情况:一是改了项目所在的路径(Claude Code 按项目路径找记录,路径一变就对不上了),二是你自己手动删了那个文件夹。
平时想备份或者迁移记录,把对应的目录整个复制走就行。