anomalyco / anomalyco/opencode
Path args with Unicode whitespace (U+00A0 / U+202F) silently fail to resolve — propose NFKC-tolerant matching at the shared resolver
@jlongster is already working on this.
Since Aug 11, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
File paths containing non-ASCII Unicode whitespace (e.g. U+00A0 NO-BREAK SPACE, U+202F NARROW NO-BREAK SPACE) silently fail to resolve when passed to a tool. By the time the path reaches a tool, these codepoints have been normalized to plain U+0020, so the string no longer byte-matches the file. Any tool that does an exact fs.existsSync then reports "file not found", while a glob against the same directory matches the real file.
This affects every tool that receives a local path — built-in read/edit/glob/grep and any MCP tool that reads files (e.g. a vision tool's image_source). The shared root cause is that the path bytes are lost in the rendering/transport chain before the resolver sees them.
Reproducible example
macOS with AppleLocale = ru_US. Take a UI screenshot → the system writes a filename such as:
Снимок экрана — 2026-08-11 в 10.07.55 PM.png
xxd of the real on-disk name contains:
... U+00A0 (NBSP) before the em-dash
... U+00A0 (NBSP) between "в" and the time
... U+202F (NARROW NBSP) before "PM"
i.e. the whitespace is not plain U+0020. When that path is then referenced (pasted into the TUI, carried in an error message, or emitted by the model as a tool argument), it arrives with U+0020 in all those positions. The tool then fails:
Image file not found: .../Снимок экрана — 2026-08-11 в 10.07.55 PM.png
while cp <dir>/*.png … (a glob) succeeds against the byte-true name.
Root cause
Lossy normalization of Unicode whitespace when a path is rendered into text (terminal / injected error message / model reproduction). Once normalized, no downstream exact-match layer can recover the original bytes — only the filesystem still has them. Cyrillic (and other non-whitespace Unicode) letters resolve fine; it is specifically the whitespace variants that break.
Verified: a path built with U+00A0 → fs.existsSync(realName) = true; the same path with U+0020 substituted = false. One codepoint of difference is enough.
Proposed fix
At the shared tool-argument path-resolution layer (the same place that logs "resolved path"), when an exact match fails, fall back to Unicode-tolerant matching against the parent directory:
// normalize both the input segment and each readdir entry, then compare
const norm = (s) => s.normalize("NFKC").replace(/\s+/g, " ").trim();
NFKC maps U+00A0/U+202F/U+2007/etc. to U+0020; folding whitespace runs handles the rest. Walk each path segment; on a literal miss, readdir the parent and take a unique NFKC-equivalent match. This recovers the byte-true path once, for all tools, instead of patching each consumer.
This is a class-level fix: it covers built-in tools and every MCP that reads local files, and it removes the need for per-MCP workarounds.
Scope / related
- #23508 — provide the full/correct image path in the error message (same family: the path string the user/agent gets is unreliable).
- #29216 — non-vision models blocked from passing images to vision-capable MCP tools.
- #33695 — image read fails with a vision-capable local model.
Workaround in use today
I'm running a local tool.execute.before plugin that NFKC-resolves path-like args for all tools (built-in + MCP), plus a per-MCP patch on the path-validation function. Both work, but the right home for this is the shared resolver. Happy to contribute a PR if a maintainer points me at the resolver's location in packages/core.
Environment
- opencode 1.18.16 (Mach-O arm64 build)
- macOS,
AppleLocale = ru_US
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.