Repository navigation
# OpenCode {env:USERPROFILE} bug - reference and analysis #43691
Description
Description
Existing issue
This bug is already reported and closed as duplicate:
-
{env:} substitution breaks on Windows paths due to unescaped backslashes in JSON #35536 — {env:} substitution breaks on Windows paths due to unescaped backslashes in JSON
- Closed as duplicate
- Reported against OpenCode 1.17.13 (Jul 6, 2026)
- Assigned to @Hona, no fix shipped as of 1.18.19
-
{env:VAR} substitutions break on Windows (backslashes in paths produce InvalidEscapeCharacter) #20640 — earlier report, closed as "incorrect root cause attribution", corrected
issue was never filed (per the {env:} substitution breaks on Windows paths due to unescaped backslashes in JSON #35536 reporter)
No fix has landed as of OpenCode 1.18.19.
The bug
OpenCode's {env:VAR} substitution replaces placeholders in the raw config
text before JSONC parsing, without JSON-escaping the substituted value. On
Windows, environment variables like USERPROFILE contain backslashes
(C:\Users\<name>), which produce invalid JSON escape sequences (\U) and make
the entire config file unparseable.
Why substitute before parse (the design rationale)
The substitute-before-parse order appears intentional. The substitute function
handles two kinds of placeholders, and one of them requires raw-text injection:
{env:VAR}— inline an environment variable value{file:path}— read an entire file and inline its raw contents
The {file:...} case is the reason substitution must happen before JSONC
parsing. A common pattern is:
"agent": {
"<harness>": {
"prompt": "{file:./<harness>/prompts/primary.md}"
}
}The file primary.md is a multi-paragraph agent prompt containing newlines,
quotes, backticks, and other characters that are not valid inside a JSON string
literal. The file's contents are meant to become part of the JSON structure
itself — not a JSON-escaped string value. If you parsed the JSON first and then
substituted into the parsed string, you would either have to JSON-encode the
file contents (which would deliver escaped strings to downstream consumers
instead of raw text, breaking the prompt), or the file would have to be valid
JSON itself (which defeats the purpose of {file:...}).
So the design is: substitute first, parse second, because {file:...} can
inject arbitrary content that is itself meant to be part of the JSON.
Why it breaks for {env:VAR}
The two placeholders have opposite needs:
| Placeholder | Content | Should be JSON-escaped? |
|---|---|---|
{file:path} |
arbitrary, may be JSON structure | No — meant to become part of the JSON |
{env:VAR} |
a string value (path, key, etc.) | Yes — it's a value inside a JSON string |
The bug is that both placeholders go through the same replace call with the
same lack of escaping. The env value is spliced into the raw text as-is, and if
it contains backslashes (as Windows paths do), the resulting text is no longer
valid JSON.
Notably, {file:...} already does the right thing — it wraps the file contents
with JSON.stringify(), which escapes backslashes. {env:VAR} does not.
Suggested fix
The fix is not "move substitution after parse" — that would break {file:...}.
Instead, {env:VAR} and {file:path} need different treatment:
-
JSON-escape
{env:VAR}substitutions only — after replacing an env
placeholder, escape backslashes (\->\\) and other JSON-special
characters in the substituted value before it lands in the raw text. Leave
{file:...}substitutions unescaped. This is the minimal fix and respects
the existing design. This is essentially what{file:...}already does via
JSON.stringify(). -
Detect context — only escape the env value if the placeholder appears
inside a JSON string literal (between double quotes). If it appears in a
position where{file:...}would (replacing a value that is itself JSON),
leave it raw. This is more correct but harder to implement with regex
replacement on raw text, which is likely why the escaping was omitted. -
At minimum, document the limitation — if the current behaviour is kept,
note that{env:...}values containing backslashes will break the config on
Windows, and recommend forward-slash paths or{env:HOME}as a workaround.
Plugins
own harness
OpenCode version
1.18.19
Steps to reproduce
Reproduction
1. Config file
~/.config/opencode/opencode.jsonc contains:
{
"instructions": [
"{env:USERPROFILE}/.config/opencode/<harness>/AGENTS.md"
]
}2. Error
ConfigJsonError: InvalidEscapeCharacter at line 34, column 5
Line 34: "C:\Users\<user>/.config/opencode/<harness>/AGENTS.md",
^
The full error includes the substituted config content showing
"C:\Users\<user>/.config/..." where \U is an invalid JSON escape.
3. Root cause
The substitute function does a raw string replacement on the config text
before it is parsed as JSONC:
// From the binary (minified):
async function d(o) {
let t = o.missing ?? "error",
r = o.text.replace(/\{env:([^}]+)\}/g, (D, n) => {
return (o.env?.[n] ?? process.env[n]) || ""
}),
// ... then {file:...} handling ...
return r;
}
// Caller:
A = yield* W.promise(() => B2.substitute({text: Y, type: "path", path: j.path, env: Q}));
G = f.jsonc(A, z); // parses substituted text as JSONCThe replacement (o.env?.[n] ?? process.env[n]) || "" inserts the raw
environment variable value into the text. On Windows:
process.env.USERPROFILE=C:\Users\<user>- After substitution:
"C:\Users\<user>/.config/opencode/<harness>/AGENTS.md" \Uis not a valid JSON escape sequence (only\ulowercase is)- JSONC parse fails
The #35536 reporter identified the same root cause in the source:
substitute() in packages/opencode/src/config/variable.ts does a plain
replace() for {env:} values, unlike {file:} which wraps with
JSON.stringify() to escape backslashes.
4. Why this matters
Any {env:...} placeholder whose value contains a backslash will break the
config. This affects at minimum:
{env:USERPROFILE}—C:\Users\<name>(Windows){env:HOMEDRIVE}+{env:HOMEPATH}—C:\+\Users\<name>- Any custom env var containing a Windows path
The {env:HOME} placeholder works on Linux/macOS because /home/<user> has no
backslashes, but on native Windows HOME is typically unset and USERPROFILE
is the correct variable.
Screenshot and/or share link
No response
Operating System
Windows 11
Terminal
Window Terminal
Activity
github-actions commentedon Aug 20, 2026
This issue might be a duplicate of existing issues. Please check:
{env:}substitution on Windows paths with backslashes producesInvalidEscapeCharacterin JSONC (closed, assigned to @Hona){env:VAR}with native Windows paths (closed, assigned to @Hona){env:VAR}substitution breaking on Windows due to backslashes (closed)If the fix from those issues has not yet shipped as of 1.18.19, it may be worth re-opening or tracking one of the existing issues rather than filing a new one.
thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊