写了 266 行 SKILL.md,Codex 死活不认——揪出 UTF-8 BOM 这个隐形杀手
写了 266 行 SKILL.md,Codex 死活不认——揪出 UTF-8 BOM 这个隐形杀手
摘要:精心写了一个 Codex Skill,文件存在、内容正确、路径没问题——但 Codex 就是找不到它。排查发现全仓 20 个 skill 中,19 个文件开头是
---,只有我一个开头是EF BB BF。本文还原完整的排查过程,讲清楚 BOM 是什么、为什么让 skill 静默失效、以及如何一劳永逸地避免。
📌 前言 / 问题场景
团队规范要求所有 commit message 遵循 Conventional Commits 格式。每次 git commit 都要手写 feat:、fix:、BREAKING CHANGE:,烦得不行。
于是你写了 200 多行 SKILL.md,做了一个 $git-commit skill——输一段改动描述,自动生成规范的 commit message。写好、测试、放到 ~/.codex/skills/git-commit/,重启 Codex。
敲下 $git-commit "修了登录页token过期不刷新的bug"。
Codex 回你一句「未找到该 skill」。你以为是触发词没写对,又把 description 里的关键词改了三版——git、commit、提交、Conventional Commits 全塞进去。
依旧不认。
这就很邪门了。文件存在、路径正确、YAML 没报错——凭什么不认?
📋 环境说明
| 组件 | 版本/说明 |
|---|---|
| Codex CLI | open-source 版本 |
| OS | Windows 11 |
| Skill 路径 | ~/.codex/skills/git-commit/SKILL.md |
| 文件编码 | UTF-8(带 BOM)← 就是它 |
| 参考对比 | 同目录下 19 个正常工作的 skill |
🔍 问题复现
- 写好 SKILL.md,放到 skills 目录
- 重启 Codex
- 输入 skill 名或触发词
预期:Codex 加载 skill,匹配触发词,开始执行。
实际:完全静默。不报错、不警告、不做任何提示——skill 被当空气。
🧭 排查过程
尝试 1:检查文件是否存在 ❌
第一反应——路径写错了?
Test-Path "~/.codex/skills/git-commit/SKILL.md"
# True
文件在,266 行,内容完整。排除。
尝试 2:怀疑 YAML 格式有问题 ❌
会不会是 description 字段的引号没转义对?翻出正常工作的 java-study-notes 对比:
正常 skill: description: Java beginner study assistant...
我的 skill: description: Write viral CSDN technical articles...
格式完全一致。而且 YAML 如果有语法错误,通常会有报错——这里连报错都没有。
尝试 3:怀疑 description 关键词匹配不上 ❌
注意到 description 里有 \"写文章\" 这种写法——PowerShell 写文件时把双引号转义了。把 \" 全部替换成 "。
重启 → 还是不认。
尝试 4:对比字节级差异 ✅
前面三步都排除了,只能往更底层挖——直接比文件原始字节:
$b = [System.IO.File]::ReadAllBytes("SKILL.md")
Write-Host $b[0].ToString('X2') $b[1].ToString('X2') $b[2].ToString('X2')
# 输出: EF BB BF
正常 skill 的前 3 个字节呢?
# java-study-notes:
# 输出: 2D 2D 2D (= "---")
破案了。
全仓 20 个 skill,遍历检查:
False | 2D 2D 2D | algorithmic-art
False | 2D 2D 2D | brand-guidelines
False | 2D 2D 2D | canvas-design
...(中间 16 个全是 2D 2D 2D)...
True | EF BB BF | csdn-viral-article ← 就这一个!
19 个正常 skill 开头都是 ---。只有我这一个开头是 EF BB BF——UTF-8 BOM。
🛠️ 解决方案
去掉 BOM,重塑文件:
# 读取内容
$content = [System.IO.File]::ReadAllText("SKILL.md")
# 用不带 BOM 的 UTF-8 重新写入
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText("SKILL.md", $content, $utf8NoBom)
# 验证
$b = [System.IO.File]::ReadAllBytes("SKILL.md")
# 前 3 字节必须是 2D 2D 2D
覆盖后重启 Codex——$csdn-viral-article 秒唤醒。
🧠 原理分析
BOM 到底是什么?
BOM(Byte Order Mark)是 UTF-8 文件开头的一个可选标记,固定值 EF BB BF,占 3 个字节。
它的原始用途是告诉文本编辑器:「我是 UTF-8 编码的,别用 GBK 猜」。但 UTF-8 本身没有字节序问题,所以 BOM 对 UTF-8 来说是多余的——大部分现代工具都不需要它。
为什么 PowerShell 会加 BOM?
Set-Content -Path "file.md" -Encoding UTF8
PowerShell 的 -Encoding UTF8 默认带 BOM。这不是 bug,是微软的设计选择——Windows 生态里有些老工具(比如记事本)靠 BOM 判断编码。想不带 BOM,得显式用:
# PowerShell 5.1+
Set-Content -Path "file.md" -Encoding UTF8NoBOM
# 或者直接用 .NET
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText("file.md", $content, $utf8NoBom)
为什么 Codex 不认 BOM?
Codex 的 YAML 解析器读取 SKILL.md 时,期望第一个字符就是 -(YAML frontmatter 的 --- 开头)。
期望: |---\nname: ...|
实际读到: |[EF BB BF]---\nname: ...|
解析器看到开头的三个陌生字节,判断「这不是一个合法的 YAML 文件」——然后静默跳过。不报错,因为它在设计上允许目录里有非 skill 文件(README、脚本等),跳过不认识的文件是正常行为。
为什么这是「静默」的?
这是最坑的地方:
- 如果 skill 不存在 → 静默(正常,可能根本没装)
- 如果 skill 格式错误 → 静默(Codex 把它当普通文件跳过)
- 如果 skill 完全正确,只是多了 BOM → 同样静默
三种情况,同一种表现。没有任何线索告诉你问题出在哪。
📝 总结
- BOM 不是 bug,是 PowerShell 默认行为——
Set-Content -Encoding UTF8会加、UTF8NoBOM不加 - Codex skill 静默失效的最难排查原因之一——没有报错、没有日志、没有任何提示
- 排查方法:直接读文件前 3 个字节,必须是
2D 2D 2D(---),不能是EF BB BF - 修复方法:用
UTF8Encoding($false)重新写入,一劳永逸 - 通用教训:任何 YAML-based 的配置系统(GitHub Actions、K8s、Ansible),如果文件「看起来对但就是不生效」,查一下 BOM——它比你想的更常见
📚 参考资料
© 本文为原创内容,转载请注明出处。
如果这篇文章帮你避开了 BOM 的坑,欢迎点赞 👍、收藏 ⭐、关注 ➕,后面还会分享更多 Codex Skill 开发中的暗坑记录。
更多推荐

所有评论(0)