Codex 写出一份看着干净的 patch。你在编辑器里打开文件,Prettier-on-save 把每一行重排:引号翻转、加上 trailing comma、加分号或去分号、断行重布。git diff 变成实际改动的两倍长。更糟的是 agent 后续想继续接它自己写的代码时,对不上自己原本的行。Pre-commit hook fail,CI lint 报错。“改了 10 行”变成 300 行。
最快的修法: 在 AGENTS.md 里加一条规则,让 Codex 在它报告 “done” 之前,先用项目锁定的 Prettier 格式化:
Run `pnpm exec prettier --write` on every file you touch.
Then `pnpm exec prettier --check` must return 0 before you finish.
这一步握手就把编辑器从”隐藏的第二个格式化器”里拿掉了。本文剩下的部分讲为什么你的第一稿还会漂(版本不一致、AGENTS.md 和 .prettierrc 打架、ESLint 跟 Prettier 抢),以及怎么让 agent 第一稿就和 Prettier 产出一致。
这不是 Codex 的 bug,是握手没做好。Agent 不知道有 .prettierrc,或者知道但 prompt 没加载它,或者跑了 format 但用的是默认设置不是项目设置。修法是让 Prettier 成为 agent”完成”定义的一部分,并把相关规则喂进 agent 的风格提示,让它第一稿就已经符合 Prettier 会产出的样子。
你属于哪一类
| 症状 | 最可能的原因 | 跳到 |
|---|---|---|
Diff 比 agent 报告的大 5 倍;prettier --check 失败 | Agent 没格式化,编辑器保存时格式化了 | 原因 1、Step 1 |
清一色的改动(全部 ' 变 "、到处加分号) | Agent 跑的 Prettier 版本不一样 | 原因 2、Step 3 |
| 还没保存,agent 的稿子就一直”不对” | AGENTS.md 风格和 .prettierrc 不一致 | 原因 3、Step 2 |
| 取决于最后跑哪个工具,文件来回翻 | ESLint 和 Prettier 都在格式化 | 原因 4、Step 4 |
| 只在 agent 写的行里出现非 ASCII(智能引号、nbsp) | Agent 输出了不可见字符 | 原因 5、Step 5 |
| 一个手对齐的块总被挪动 | 改动落在 // prettier-ignore 区里 | 原因 6、Step 6 |
常见原因
按命中率排序。
1. Agent 没跑 Prettier,编辑器跑了
Agent 改完保存、宣布”done”。在你机器上编辑器 on-save Prettier 又重排了一遍。看着干净的输出最后一秒被改了形。
如何判断:git diff 比 agent 报告的大很多。跑 prettier --check <file>——会有不少行 fail。
2. Agent 跑的 Prettier 和项目的不是同一个
pnpm prettier --write vs npx prettier@3.6 --write vs 全局 prettier,不同版本产出不同。Prettier 在小版本之间会带来格式上的微调(3.5 加了 objectWrap 和 experimentalOperatorPosition 选项,3.6 上线了实验性的 fast CLI,截至 2026 年 6 月这条线在 3.8.x),所以一个解析到 3.8 的全局 prettier,不会和项目锁定在 3.3 的产出一致。
如何判断:项目里跑 pnpm exec prettier --version(用项目本地解析,不是全局)和 agent 跑的不一样。Diff 是清一色的格式噪声(比如每个 ' 都变成 ",或者每个多行调用都加上 trailing comma)。
3. AGENTS.md 风格提示和 .prettierrc 不一致
AGENTS.md 说”用单引号”,但 .prettierrc 是 "singleQuote": false。Agent 听 AGENTS.md、保存时 Prettier 赢——两边在打架。
如何判断:手动对比 AGENTS.md 的风格主张和 .prettierrc / prettier.config.js。任何不一致 = 永远的吵闹 diff。
4. Prettier 和 ESLint 两个格式化器混用
ESLint 配 eslint-plugin-prettier + 独立 on-save Prettier,对 arrow paren、JSX trailing comma 排序略有不同。Agent 挑一个,工具链挑另一个。
如何判断:取决于最后跑哪个工具文件会不一样。跑 pnpm prettier --check . && pnpm eslint .——如果 prettier --write 后两个都过,但 eslint --fix 又把行改回去,就是有冲突。
5. Agent 输出了 Prettier 会改写的不可见字符
智能引号、不间断空格、en-dash——Codex 有时在 comment 或 string 里塞这些。Prettier(或某些插件)会标准化。
如何判断:cat -A <file> | grep -E '\xc2\xa0|\xe2\x80' 能找到非 ASCII。对比 agent 输出文本。
6. 文件有 // prettier-ignore 指令
现有代码用 // prettier-ignore 保留手对齐的块。Agent 在块内部改——Prettier 尊重 ignore,但 agent 的对齐和原对齐对不上。
如何判断:grep -rn "prettier-ignore" <files> 列出保护区,对照 agent 编辑的区域。
动手前先确认
- 存快照:
git stash或git diff > before.patch,方便清晰对比。 - 记 Prettier 版本:
cat package.json | grep '"prettier"'和pnpm exec prettier --version(用项目本地解析,不是全局)。 - 记编辑器的 Prettier 集成:VS Code 里的
editor.formatOnSave、editor.defaultFormatter(应为esbenp.prettier-vscode)和prettier.requireConfig。注意:自 Prettier VS Code 扩展 v12.1.0(2026 年 1 月)起,扩展不再自动检查editor.defaultFormatter是否设成 Prettier、editor.formatOnSave是否为true,所以这两项要手动确认,别想当然以为已经接好了。
需要收集的信息
.prettierrc/prettier.config.js/package.json#prettier内容。- AGENTS.md 里关于风格的章节。
- 保存时被重排的具体文件(对比
git diff行数和 agent 实际写的)。 - 用的编辑器 + 扩展(VSCode + Prettier 扩展等)。
- 是否装了
prettier-eslint或eslint-config-prettier。
最短修复路径
按收益从高到低。
Step 1:把 Prettier 写进 agent 的”完成”循环
放进 AGENTS.md(Codex 会在 session 启动时自动读它),或放进任务 prompt:
每次改完文件,跑:
pnpm exec prettier --write <被改文件>
确认:pnpm exec prettier --check <被改文件> 返回 0。
非 0 就修完再说完成。
按 OpenAI 的 Codex 指南,AGENTS.md 是这条规则的长期归宿——它每次运行都会加载进上下文,你不用在每个 prompt 里重复。这样 agent 最后一动就和编辑器会产出的一样:保存时零重排。大仓库上,prettier --cache(Prettier 3.6+ 默认开启缓存)能让每次改完的检查保持快。
Step 2:AGENTS.md 风格主张对齐 .prettierrc
打开 .prettierrc(或 prettier.config.js),每个选项翻译到 AGENTS.md:
## 格式化(与 .prettierrc 一致 —— 不要偏离)
- printWidth: 100
- semi: false → 行尾不加分号
- singleQuote: true → 用单引号
- trailingComma: "all"
- arrowParens: "always" → (x) => x 不是 x => x
- bracketSpacing: true → { foo } 不是 {foo}
Codex 的第一稿就和 Prettier 输出一致——消除 diff 膨胀。
Step 3:钉死 agent 调用的 Prettier 版本
强制走项目锁的版本:
每次改完跑:
pnpm exec prettier --write <files>
# 不要:npx prettier --write (可能下到更新的 Prettier,比如 3.8 vs 你锁的 3.3)
# 不要:prettier --write (可能解析到全局装的)
pnpm exec(或 yarn exec / npm exec)走 lockfile 里的版本。在 package.json 里钉死确切版本(比如 "prettier": "3.8.4",不是 "^3.0.0"),这样 agent、你的编辑器、CI、队友跑的是逐字节相同的格式化器。
Step 4:彻底解决 ESLint + Prettier 冲突
修法是 eslint-config-prettier,它关掉所有和 Prettier 重叠的 ESLint 规则,让两个工具不再抢:
pnpm add -D eslint-config-prettier
如果你用的是 ESLint 9 flat config(eslint.config.js,ESLint 9 起的默认),导入 /flat 入口,并放在数组最后让它生效:
// eslint.config.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import eslintConfigPrettier from 'eslint-config-prettier/flat'
export default [
js.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier, // 必须放最后
]
如果还在用旧的 .eslintrc 格式,在 extends 里把 'prettier' 放最后:
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier']
不要用 eslint-plugin-prettier 把 Prettier 当成一条 ESLint 规则来跑。截至 2026 年,Prettier 维护者不推荐这种做法:它会让 ESLint 变慢、把格式差异变成红波浪线”错误”、还让 agent 要同时满足两个格式化器。保持分工干净——ESLint 管正确性,Prettier 管格式。这样之后,eslint --fix 和 prettier --write 输出一致,不再互相改回去。
Step 5:清理 agent 输出里的不可见字符
任务 prompt 加一段:
输出规则:
- 代码里只用 ASCII(不要智能引号、不要 en-dash、不要 nbsp)
- 注释:标准 ASCII " ' - --
- 字符串字面量:仅按用户字面要求
偏执一点的话改完跑一遍 sanitizer:
sed -i 's/\xc2\xa0/ /g; s/\xe2\x80\x9c/"/g; s/\xe2\x80\x9d/"/g' <files>
Step 6:显式处理 prettier-ignore 区域
任务 prompt 里列出受保护区:
以下块是 `// prettier-ignore` 保护区——按原对齐保留,非必要不要改:
- src/data/timezones.ts:42-78
- src/config/menus.tsx:120-180
必须改的话,缩进 / 对齐按字节对齐到现有的。
Step 7:可选——agent 跑期间关编辑器的 format-on-save
如果实在让 agent 的 Prettier 对不齐编辑器的,暂时:
"editor.formatOnSave": false
会话结束时自己跑 Prettier。把编辑器从隐藏 actor 拿掉。
怎么确认已经修好
- Agent 输出的
git diff:行数应该和 agent 报告一致,不是 5 倍。 - Agent 完成后立刻
pnpm prettier --check <files>返回 0。 - 在编辑器打开文件、什么都不动、保存:零变更。
- Pre-commit hook(
lint-staged+prettier --check)第一次就过。
长期预防
- AGENTS.md 格式化章节从
.prettierrc自动生成,它们就不会漂。 - 每个 agent 任务模板都包含”改完文件先 prettier 再完成”。
- Agent tool 调用里用
pnpm exec(或同类),不用全局prettier。 - 装
eslint-config-prettier,ESLint 不会和 Prettier 吵。 - 加 CI job 跑
prettier --checkPR 检查——merge 之前抓 drift。 - 想要 ASCII 纯度,加 pre-commit hook 拒收
.ts/.js里的非 ASCII。
常见坑
- Agent 改完之后整个项目跑
prettier --write——掩盖 agent 真正改了哪些行、diff 膨胀。 - 改了 AGENTS.md 风格章节但忘了改
.prettierrc(或反过来)——下个月又开始打架。 - 信任
npx prettier不走pnpm exec——版本锁定被静默忽略。 editor.formatOnSave开着的时候去”只是看看”agent diff——你一点进文件就被重排了。- 把 trailing comma 不一致当美观问题忽略——它打断
git blame、之后每个 diff 都膨胀。
常见 FAQ
Q:我把 Prettier 加进 agent 循环了,编辑器保存还在重排,为什么?
两个常见嫌疑。第一,prettier.requireConfig:为 false 时编辑器用 Prettier 默认设置、忽略你的 .prettierrc,设成 true 就只在找到 config 时才跑 Prettier。第二,自 Prettier VS Code 扩展 v12.1.0(2026 年 1 月)起,扩展不再自动确认自己是当前格式化器,所以要显式设 "editor.defaultFormatter": "esbenp.prettier-vscode" 和 "editor.formatOnSave": true(如果有别的格式化器在抢,用 "[typescript]" 这样的按语言块来限定)。
Q:我们迁到 ESLint 9 flat config,eslint-config-prettier 不灵了,变了什么?
flat config 用的是另一种导入。用 import eslintConfigPrettier from 'eslint-config-prettier/flat',并放在 export default [ ... ] 数组最后。不带 /flat 的裸 eslint-config-prettier、以及 extends: ['prettier'] 字符串,都是旧 .eslintrc 格式的写法,在 flat config 里会静默地什么都不做。
Q:我们团队用 Biome / dprint,不是 Prettier,修法一样吗?
一样——把 Prettier 命令换成 biome format --write 或 dprint fmt。结构性修法(formatter 进 agent 循环 + 风格提示对齐 config)完全一样。
Q:能让 Codex 直接别格式化吗?
可以,但你得有一套清晰的”agent 之外统一格式化”流程。Commit 时 diff 还是会吵。不如让 agent 第一稿就对齐 Prettier 产出。
Q:.prettierignore 呢?
同样的——确保 AGENTS.md 列出 agent 不该碰的 pattern,和 .prettierignore 一致。否则 agent 改了一个生成文件、Prettier 拒绝格式化——又是不一致。
相关阅读
- Codex 漏项目特定约定
- Codex 生成的代码不合现有 style
- Codex Agent 任务执行到一半就停
- Codex Agent 调用工具次数失控
- Codex patch 和现有代码冲突
- Codex 环境配置失败
- Codex Agent 跳过含二进制数据的文件:6 个原因 + probe 替代方案
- Codex 无法访问私有仓库:6 个权限层 + 排查清单
- Codex 写的 PR 描述太敷衍:怎么逼出结构化 before/after
- Codex 遇到 merge conflict 就卡住或瞎选边:rebase 策略和 AGENTS.md 兜底
- Codex 加了包但 lockfile 没动:怎么强制同步更新
- Codex 又造了一个和现有类型重名的 interface:怎么让它先搜再写
- Codex 说 “测试通过”,其实跳过了失败用例:如何强制诚实报告
- Codex 一遇大 diff 就停下来:怎么把任务切小
外部参考
- eslint-config-prettier — flat config 配置(
/flat导入 + 顺序) - Codex:用 AGENTS.md 写自定义指令(把 format-on-done 规则放哪里)
- Prettier 配置选项(需要镜像到 AGENTS.md 的选项名)