2026 年 7 月 12 日,一名开发者发文称,Claude Code 在检查数据库迁移问题时,误将生产数据库当成临时测试数据库使用,导致生产数据库的全部表结构被删除并重新创建。
更令人意外的是,Claude Code 完成操作后并未发现异常,反而报告称生产环境一切正常,“没有进行任何修改”。
根据当事人的说法,Claude Code 最初正在排查测试环境中的数据库迁移记录与实际结构不一致问题。随后,开发者追问生产环境是否也受到了影响,希望 Claude Code 只对生产数据库进行检查。
Claude Code 随即运行 Prisma 数据库迁移工具的 migrate diffcode> 命令,用于比较迁移文件与生产数据库结构之间是否存在差异。
问题出在命令参数上。
Claude Code 将生产数据库地址同时设置为待检查数据库和“影子数据库”。
影子数据库是 Prisma 用于临时重放迁移记录的工作空间,系统默认其内部数据可以随时清除,并不适合存放真实业务数据。
Prisma 官方文档明确警告,实际数据库地址与影子数据库地址不得使用相同配置,否则可能删除数据库中的全部数据。影子数据库在运行过程中会被重置,并重新执行已有的迁移记录。
由于生产数据库被错误指定为影子数据库,Prisma 删除了原有表结构,并重新执行了 116 个迁移文件。数据库的表和字段虽然重新建立,但原有业务数据已不在这些新建的空表中。
随后,Prisma 返回“没有需要生成的迁移”结果。
Claude Code 将这一结果理解为生产数据库与迁移记录完全一致,因此判断数据库状态正常。实际上,两者之所以完全一致,是因为生产数据库刚刚按照这些迁移文件被重新创建。
Claude Code 最终向开发者报告称:“生产环境完全正常,没有进行任何修改,所有操作均为只读。”
当事人随后检查了约 2000 份 Claude Code 本地会话记录,找到执行相关命令的会话。命令运行时间与数据库中第一条重新生成的用户记录相差约 72 秒,由此确认了数据库被重建的时间。
幸运的是,该公司使用的托管 PostgreSQL 数据库支持按时间点恢复。开发者最终将数据库恢复至事故发生约 2.5 小时前的状态,全部数据得以找回,未造成永久性损失。
原贴翻译:
我发出这篇帖子,是因为这种故障方式非常隐蔽。
我认为,即使换成人类开发者,或使用其他 AI 编程工具,也有不少人可能会踩中同一个坑。
事情的背景是这样的,当时,我让一个 AI 编程代理 Claude Code 排查测试环境中的数据库迁移偏移问题。
随后,我又追问了一句:“生产环境也受到影响了吗?”
于是,它开始检查生产环境是否正常。而生产数据库恰恰是我明确不想触碰的唯一一个数据库。
最终摧毁数据库的是下面这条命令:
prisma migrate diff \
--from-migrations ./prisma/migrations \
--to-url "$PROD" \
--shadow-database-url "$PROD" # <-- 这里指向的是生产数据库
不了解 Prisma 工作机制的人,可能很难第一眼看出这条命令为什么如此致命。
当 prisma migrate diff 使用 --from-migrations 参数时,Prisma 必须找一个数据库,实际执行这些迁移文件,从而计算迁移完成后的数据库结构。
这个数据库就是 --shadow-database-url 指定的“影子数据库”。
Prisma 默认认为影子数据库只是一个可以随时丢弃的临时工作区,因此会删除其中原有的数据库结构,再重新执行全部迁移。
Claude Code 将生产数据库地址传给了影子数据库参数,相当于告诉 Prisma:将生产环境作为临时工作区使用。
于是,Prisma 删除了生产数据库的全部表结构,并重新执行了 116 个迁移文件,最终只留下了一批没有业务数据的空表。
最讽刺的是,比较结果随后显示:
This is an empty migration.
也就是“这是一个空迁移”,没有发现任何结构差异。
Claude Code 将其理解为:“生产数据库与最新迁移版本完全一致,运行正常。”
但结果之所以没有差异,是因为生产数据库就在一秒钟前,刚刚按照这些迁移文件被完整删除并重新创建。
Claude Code 最后的回复甚至明确写道:
生产环境完全正常。没有进行任何修改,所有操作均为只读。
事情为什么会发生
坦率地说,原因有以下几点。
第一,“只读”并不是由 migrate diff 这个子命令本身决定的,而是由它搭配的参数决定的。
例如:
migrate diff --from-url ... --to-schema-datamodel
确实是只读操作。
但是:
migrate diff --from-migrations ... --shadow-database-url
并不是只读操作。
两者使用的是同一个 migrate diff 子命令,但实际行为完全不同。
第二,Claude Code 在几分钟前刚刚对测试环境运行过同样的命令,而且表面上看起来“成功了”。
实际上,它当时也悄悄清空了测试数据库。
但测试数据库本来就是一个正在重新构建、状态已经异常的副本,所以这次重置没有被人发现。
第三,生产数据库所有者账号的连接字符串,就放在一个临时 Git 工作目录的 .env 文件中。
它与一次常规命令之间,只隔着一个环境变量。
如何查明事故原因:
Claude Code 会将每一次会话以 JSONL 格式保存在本地磁盘中。
我在大约 2000 份会话记录中搜索具有破坏性的命令特征,最终找到了相关会话。
致命命令的执行时间,与数据库中第一条“重新生成的新用户”记录的出现时间,相差不到 72 秒。
两份相互独立的证据指向了同一个时间点,因此基本锁定了事故原因。
有些荒诞的是,这次数据库清空事件,最后是通过读取 AI 自己留下的操作记录还原出来的。
数据如何恢复:
我们使用的是支持按时间点恢复的托管 PostgreSQL 数据库。
最终,我们将数据库恢复到了事故发生约 2.5 小时前的状态。
数据全部恢复,没有永久丢失。
这也是我的公司今天还能继续存在的全部原因。
这次事故的教训:
--shadow-database-url 指向的数据库,本质上就是一个你已经授权工具随时删除的数据库。
绝对不要将它指向任何真实数据库。
在 Prisma 的迁移工具中,只有 migrate deploy 适合在生产环境中使用。
db push、migrate dev,以及使用 --from-migrations 参数的 migrate diff,都可能执行重置或删除操作。
此外,执行得更彻底,并不等于更安全。
Claude Code 选择了最严格、最权威的验证方式,但恰恰是这种验证方式带来了副作用。
因为在数据库工具中,所谓“权威验证”通常意味着,工具需要在某个地方真正创建和还原数据库状态。
真正的系统性解决方案,并不是“少相信 AI”。
关键在于,不要让生产环境的可写凭据出现在开发人员或 AI 代理可以直接访问的终端环境中。
AI 的自主性只会放大现有权限控制的效果:你设置了哪些安全边界,它就会放大哪些安全边界;你没有设置哪些边界,它也会放大这些缺口。
即使没有 AI,同样的权限隔离措施,也能拦住一名晚上 10 点半因疲劳而误操作的开发者。
欢迎提问。
有需要的话,我也可以公布一份完整的事故分析,包括取证证据,例如 pg_class.reltuples = -1、_prisma_migrations 记录表消失等细节。
云头条声明:如以上内容有误或侵犯到你公司、机构、单位或个人权益,请联系我们说明理由,我们会配合,无条件删除处理。
热门跟贴