Skip to content

# OpenCode {env:USERPROFILE} bug - reference and analysis #43691

Closed as not planned

Description

@parisj

Description

Existing issue

This bug is already reported and closed as duplicate:

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:

  1. 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().

  2. 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.

  3. 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 JSONC

The 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"
  • \U is not a valid JSON escape sequence (only \u lowercase 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

  1. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    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.

  2. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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