Describe the bug
A UTF-8 BOM on an instruction file silently drops its frontmatter. apm compile exits 0, the declared applyTo scope is lost so the rule is applied globally, and the raw frontmatter block is emitted verbatim into the compiled context file.
load_frontmatter reads with encoding="utf-8" (src/apm_cli/utils/yaml_io.py:492), not utf-8-sig, so the BOM survives as a literal before the line-1 --- and the fence no longer matches.
The BOM then propagates into the generated CLAUDE.md, where APM's own audit flags it: CLAUDE.md contains 1 hidden character(s).
This is a Windows-shaped trigger. PowerShell 5.1's Out-File -Encoding utf8 and > both write a BOM, as does Notepad, so a package authored on Windows can carry one without the author ever seeing it.
To Reproduce
Two instruction files, identical except for the BOM:
fm = '---\ndescription: Scoped rule\napplyTo: "**/*.py"\n---\n# Style\nUse type hints.\n'
Path('.apm/instructions/nobom.instructions.md').write_bytes(fm.encode('utf-8'))
Path('.apm/instructions/withbom.instructions.md').write_bytes(fm.encode('utf-8-sig'))
$ apm compile --target claude
[!] .apm/instructions/withbom.instructions.md: No 'applyTo' pattern specified
-- instruction will apply globally
[!] CLAUDE.md contains 1 hidden character(s) -- run 'apm audit --file ...' to inspect
$ echo $?
0
Generated CLAUDE.md puts the BOM'd file under ### Global Instructions with its metadata as body text:
### Global Instructions
---
description: Scoped rule
applyTo: "**/*.py"
---
# Style
while the identical no-BOM file lands correctly under ### Files matching **/*.py.
Expected behavior
Decode with utf-8-sig so a leading BOM is consumed, or fail loudly rather than silently downgrading a scoped rule to global.
Environment
- OS: Windows 11 Home 26200
- Python: 3.13.13
- APM: 0.28.0, verified on
main at 88cf78a
Additional context
Not a regression from #2666, which touches this function: I ran the same case on main (88cf78a) and on that PR's head (d27ed4e) and both lose the frontmatter identically. #372 handled a BOM in strip_dangerous only; this is the loader path.
Related failure profile to #2663 and #2624: exit 0 with a quietly wrong result.
I used an AI assistant to prepare this report. I ran the reproduction, the exit code, and the two-ref comparison myself on the machine described above.
Describe the bug
A UTF-8 BOM on an instruction file silently drops its frontmatter.
apm compileexits 0, the declaredapplyToscope is lost so the rule is applied globally, and the raw frontmatter block is emitted verbatim into the compiled context file.load_frontmatterreads withencoding="utf-8"(src/apm_cli/utils/yaml_io.py:492), notutf-8-sig, so the BOM survives as a literalbefore the line-1---and the fence no longer matches.The BOM then propagates into the generated
CLAUDE.md, where APM's own audit flags it:CLAUDE.md contains 1 hidden character(s).This is a Windows-shaped trigger. PowerShell 5.1's
Out-File -Encoding utf8and>both write a BOM, as does Notepad, so a package authored on Windows can carry one without the author ever seeing it.To Reproduce
Two instruction files, identical except for the BOM:
Generated
CLAUDE.mdputs the BOM'd file under### Global Instructionswith its metadata as body text:while the identical no-BOM file lands correctly under
### Files matching **/*.py.Expected behavior
Decode with
utf-8-sigso a leading BOM is consumed, or fail loudly rather than silently downgrading a scoped rule to global.Environment
mainat88cf78aAdditional context
Not a regression from #2666, which touches this function: I ran the same case on
main(88cf78a) and on that PR's head (d27ed4e) and both lose the frontmatter identically. #372 handled a BOM instrip_dangerousonly; this is the loader path.Related failure profile to #2663 and #2624: exit 0 with a quietly wrong result.
I used an AI assistant to prepare this report. I ran the reproduction, the exit code, and the two-ref comparison myself on the machine described above.