fix: read Windows-authored agent .md + credentials.json as UTF-8 - #133
Merged
Salil Das (sadlilas) merged 2 commits intoAug 19, 2026
Merged
Conversation
Two Windows-only text-decoding gaps in the CLI/lib. Both are silent or
misleading failures rather than clean errors.
1. spawn.hydrate_agent_overlay read the agent .md as encoding="utf-8". A file
authored on Windows (Notepad, PowerShell Out-File/Set-Content) is UTF-8 WITH
a BOM, so the retained leading U+FEFF makes `text.startswith("---")` False and
the ENTIRE file is silently treated as a plain instruction -- tools, hooks,
model_role, and meta are dropped from the sub-agent overlay with no error or
warning. A Windows-authored custom agent then spawns with the wrong (or no)
tools. Now reads encoding="utf-8-sig" (strips a leading BOM if present;
identical to utf-8 otherwise).
2. auth._load_credentials read credentials.json with a bare read_text() (no
encoding). On Windows that decodes with cp1252. Three distinct problems, all
now closed:
- A BOM-prefixed credentials.json (same Windows tooling as above) decoded
into a string with a leading U+FEFF, and json.loads then failed with
"Unexpected UTF-8 BOM (decode using utf-8-sig)" -- reported to the user as
"not valid JSON", which is both wrong and unactionable. Now reads
encoding="utf-8-sig", matching hydrate_agent_overlay.
- Bytes that are genuinely not valid UTF-8 (a cp1252-era file from an older
build) raised UnicodeDecodeError, which the surrounding try caught only
json.JSONDecodeError for. It propagated uncaught and crashed `auth
list/status/set` plus every provider credential lookup behind
`run`/`models`/`serve` with a raw traceback. Now caught and re-raised as a
click.ClickException naming the file and the remediation.
- The atomic write is pinned to encoding="utf-8" for symmetry, so we never
author a non-UTF-8 credentials file ourselves.
No-op on POSIX (utf-8 and utf-8-sig are identical for BOM-less files).
Verified behaviorally against the real functions -- 13/13 checks, covering:
BOM agent .md yields tools/model_role/meta and a frontmatter-free instruction;
BOM-less agent .md parses identically (no regression); BOM + non-ASCII body
decodes correctly; invalid-UTF-8 credentials raise ClickException with the file
path and `auth clear --force` hint; BOM-prefixed credentials.json now loads;
valid and malformed JSON both behave exactly as before.
Fast gate clean: ruff check, ruff format --check, pyright src/ (0 errors).
Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Salil Das (sadlilas)
force-pushed
the
fix/windows-agent-md-and-credentials-utf8
branch
from
August 18, 2026 22:59
ca21a12 to
2742372
Compare
When a credentials file is not valid UTF-8, the auth command now raises a clean ClickException with remediation guidance. However, the resolver functions (used by 'auth list' and other credential lookups) deliberately never raise — a single bad file must not brick every subsequent invocation — so they caught that exception and logged it at DEBUG level only. In practice, this rendered the fix invisible: users saw every provider as <not set> with no stderr output, making them believe their credentials were lost rather than unreadable. This commit keeps the never-raise contract but makes the degradation visible. A new _warn_credentials_unreadable_once() helper emits the remediation hint to stderr exactly once per process, guarded by a module-level latch. Both resolve functions now route their caught ClickException through it. The latch prevents duplicate warnings when resolving credentials for multiple providers (a single 'auth list' can resolve 10+ times). Stderr keeps stdout clean for callers that parse it. Result: a corrupt credentials file now exits cleanly (0) with exactly one diagnostic line on stderr, carries the file path and parse failure for troubleshooting, and correctly degrades providers to <not set> — visible and actionable, rather than silent and misleading. Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Salil Das (sadlilas)
deleted the
fix/windows-agent-md-and-credentials-utf8
branch
August 19, 2026 01:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three text-decoding gaps that bite files authored on Windows. Each one fails silently or misleadingly rather than raising a clean error. All three are no-ops on POSIX and for any BOM-free file —
utf-8andutf-8-sigdecode identically when there is no BOM.Windows tooling — Notepad, Windows PowerShell 5.1
Set-ContentandOut-File— writes UTF-8 with a byte-order mark by default. Reading such a file as plainutf-8preserves the mark as a leadingU+FEFF, which is invisible in every editor but sits at position 0 and defeats any check anchored at the start of the text.1. A Windows-authored agent
.mdsilently spawns with no tools.spawn.hydrate_agent_overlayread the file asencoding="utf-8". The survivingU+FEFFmakestext.startswith("---")False, so the entire file is treated as a plain instruction —tools,hooks,model_role, andmetaare dropped from the sub-agent overlay with no error and no warning. The sub-agent then runs with the wrong tools, or none. Now readsencoding="utf-8-sig".2. A BOM-prefixed
credentials.jsonis reported as corrupt when it isn't.auth._load_credentialsread the file with a bareread_text(). The leadingU+FEFFreachedjson.loads, which failed with "Unexpected UTF-8 BOM (decode using utf-8-sig)" — surfaced to the user as "not valid JSON". The file is perfectly valid JSON; the message is both wrong and unactionable. Now readsencoding="utf-8-sig", matchinghydrate_agent_overlay.3. A non-UTF-8
credentials.jsoncrashes the CLI with a raw traceback. The bareread_text()also decoded with the platform default — cp1252 on Windows. Bytes that are genuinely not valid UTF-8 (a file written by an older build under a non-UTF-8 locale, or hand-edited in a legacy encoding) raiseUnicodeDecodeError, but the surroundingtrycaught onlyjson.JSONDecodeError. It propagated uncaught and killedauth list/status/setplus every provider credential lookup behindrun,models, andserve. Now caught and re-raised as aclick.ClickExceptionnaming the file and the remediation.The atomic write is pinned to
encoding="utf-8"for symmetry, so we never author a non-UTF-8 credentials file ourselves.Evidence — teeth both ways
Native Windows (Python 3.14.3), the real
hydrate_agent_overlay()over a BOM+CRLF agent.md:Baseline drops all overlay config, silently. The fix recovers it.
Verified behaviourally against the real functions — 13/13 checks, each run against both the unfixed and fixed code so the failure is confirmed to exist before it is confirmed to be gone:
.mdyieldstools/model_role/meta.mdinstruction is frontmatter-free.mdparses identicallycredentials.jsonloadscredentials.jsonUnicodeDecodeErrortracebackClickExceptionwith path +auth clear --forcehintFast gate clean:
ruff check,ruff format --check,pyright src/— 0 errors.Companion fix upstream
The same class of bug exists in
amplifier-foundation's two frontmatter parsers, which sit behind agent metadata, mode metadata, and markdown bundle loading. A BOM there silently empties the parsed frontmatter — a mode declaringdefault_action: blockenforces nothing, and a bundle loses itsnameandversionso it does not resolve.Fixed separately in microsoft/amplifier-foundation#308 (
io/frontmatter.pystrips a leading BOM;bundle_docs/frontmatter.pyreads withutf-8-sig; 14 regression tests). That PR is independent — neither PR blocks the other, and the two touch disjoint code.Limits
One Windows machine, one Python (3.14.3). This repo's CI is
ubuntu-latestonly — there is no Windows leg yet. The BOM behaviour itself is platform-independent (a BOM defeatsstartswith("---")on any OS), so the agent.mdfix is verifiable everywhere; the crash-vs-recover path forcredentials.jsonwas exercised on the box.