Codex Skill 因 UTF-8 BOM 无法加载的排查与修复
记录 Codex skill 明明存在却不显示时,如何检查 SKILL.md 文件头是否带 UTF-8 BOM,并用 PowerShell 转换为无 BOM UTF-8。
现象
Codex skill 已经放在 skills 目录里,例如:
C:\Users\<username>\.codex\skills\<skill-name>\SKILL.md
目录结构、SKILL.md、YAML frontmatter 看起来都正常,但重启 Codex 之后,这些 skill 仍然没有出现在当前会话的 skill 列表中。
原因
Codex 识别 skill 时,会先读取每个 skill 目录下的 SKILL.md,并要求文件开头是 YAML frontmatter 分隔符:
---
name: your-skill-name
description: ...
---
正常情况下,文件头的前三个字节应该是:
2D-2D-2D
如果文件保存成了带 BOM 的 UTF-8,真实字节头会变成:
EF-BB-BF-2D-2D-2D
其中 EF-BB-BF 是 UTF-8 BOM。肉眼看文件仍然是从 --- 开始,但 loader 看到的第一个字节不是 -,就可能误判为缺少 YAML frontmatter。
参考:Codex Skill 明明在目录里,为什么就是不显示?
检查单个文件头
在 PowerShell 中检查某个 SKILL.md 的前 6 个字节:
$path = 'C:\Users\<username>\.codex\skills\<skill-name>\SKILL.md'
$bytes = [System.IO.File]::ReadAllBytes($path)
($bytes[0..5] | ForEach-Object { $_.ToString('X2') }) -join '-'
如果输出是:
EF-BB-BF-2D-2D-2D
说明文件带 UTF-8 BOM。
如果输出是:
2D-2D-2D-0D-0A-6E
或者前三个字节是 2D-2D-2D,说明文件直接以 --- 开头,符合 Codex loader 对 frontmatter 的预期。
批量扫描 Skills 目录
可以批量扫描一个 skills 根目录下所有 SKILL.md:
$root = 'C:\Users\<username>\.codex\skills'
Get-ChildItem -Path $root -Directory |
Where-Object { $_.Name -ne '.system' } |
Sort-Object Name |
ForEach-Object {
$path = Join-Path $_.FullName 'SKILL.md'
if (Test-Path -LiteralPath $path) {
$bytes = [System.IO.File]::ReadAllBytes($path)
$first6 = ($bytes[0..([Math]::Min(5, $bytes.Length - 1))] |
ForEach-Object { $_.ToString('X2') }) -join '-'
$hasBom = $bytes.Length -ge 3 -and
$bytes[0] -eq 0xEF -and
$bytes[1] -eq 0xBB -and
$bytes[2] -eq 0xBF
$startsWithFrontmatter = $bytes.Length -ge 3 -and
$bytes[0] -eq 0x2D -and
$bytes[1] -eq 0x2D -and
$bytes[2] -eq 0x2D
[PSCustomObject]@{
Skill = $_.Name
HasBom = $hasBom
StartsWithFrontmatter = $startsWithFrontmatter
FirstBytes = $first6
Path = $path
}
}
} |
Format-Table -AutoSize
重点看两个字段:
HasBom 应该是 False。
StartsWithFrontmatter 应该是 True。
转换为无 BOM UTF-8
如果已经确认某些 SKILL.md 带 BOM,可以用下面的 PowerShell 脚本批量转换。脚本只会处理带 BOM 的文件,其他文件不会重写。
$root = 'C:\Users\<username>\.codex\skills'
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
Get-ChildItem -Path $root -Directory |
Where-Object { $_.Name -ne '.system' } |
ForEach-Object {
$path = Join-Path $_.FullName 'SKILL.md'
if (-not (Test-Path -LiteralPath $path)) {
return
}
$bytes = [System.IO.File]::ReadAllBytes($path)
$hasBom = $bytes.Length -ge 3 -and
$bytes[0] -eq 0xEF -and
$bytes[1] -eq 0xBB -and
$bytes[2] -eq 0xBF
if ($hasBom) {
$text = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
[System.IO.File]::WriteAllText($path, $text, $utf8NoBom)
"FIXED $path"
}
}