Skip to content

[BUG] UTF-8 BOM on an instruction file silently drops its frontmatter: applyTo scope lost, metadata leaked into compiled output, exit 0 #2683

Description

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions