Motivation
Today the opencode/ snippet directories (~/.config/opencode-sandbox/opencode/ and .opencode-sandbox/opencode/) serve a single purpose: every *.json/*.jsonc/*.json5 file under them is deep-merged into one opencode.json provisioned at /home/dev/.config/opencode/opencode.json in the VM.
This conflation is a latent bug and a limitation:
- A user who wants to ship other opencode runtime files (e.g.
tui.json, AGENTS.md, agents/**, commands/**, plugins/**, themes/**) has no dedicated mechanism: those files are either merged incorrectly into opencode.json (any JSON file) or must be wired through the separate home.yaml manifest.
auth.json (credentials) lives in ~/.local/share/opencode/ and is already out of scope; it stays on the existing home.yaml path.
Proposed behavior
Make the opencode/ directory a verbatim mirror of the VM's ~/.config/opencode/ directory, while keeping the opencode.json snippet-merge behavior in an explicit, well-separated subdirectory.
- Treat the top level of
opencode/ as a 1:1 verbatim mirror of ~/.config/opencode/: every file and subdirectory is provisioned verbatim, preserving its relative path.
- Keep deep-merging of
opencode.json snippets, but relocate it into a dedicated subdirectory (opencode/config-snippets/) so it cannot collide with verbatim files.
- A top-level file named
opencode.json/opencode.jsonc/opencode.json5 remains special: it is deep-merged (treated as a snippet), never copied verbatim. The merged output always wins over any raw copy.
- Fix the latent bug where a non-
opencode.json JSON file (e.g. tui.json) at the top level was wrongly merged into opencode.json.
- Document the resulting breaking change and provide a migration note.
Non-goals
- Provisioning
~/.local/share/opencode/ (e.g. auth.json) — stays on the home.yaml mechanism.
- Any write-back / sync of
auth.json from the VM to the host.
- A
share/ mirror directory.
- Interactive-login flows inside the VM.
Alternatives considered
- Keep only the
home.yaml manifest for non-opencode.json files — works but adds per-file wiring and doesn't fix the merge-collision bug for JSON files at the top level.
- A dedicated
share/ mirror alongside config/ — rejected as out of scope (see non-goals).
Design spec
Full design spec (2026-08-28-opencode-config-mirror-design.md)
Design: opencode config directory mirror + verbatim provisioning
Date: 2026-08-28
Status: Draft (pending review)
Problem
Today the opencode/ snippet directories (~/.config/opencode-sandbox/opencode/ and
.opencode-sandbox/opencode/) serve a single purpose: every *.json/*.jsonc/*.json5 file under them is
deep-merged into one opencode.json provisioned at /home/dev/.config/opencode/opencode.json in the VM.
This conflation is a latent bug and a limitation:
- A user who wants to ship other opencode runtime files (e.g.
tui.json, AGENTS.md, agents/**,
commands/**, plugins/**, themes/**) has no dedicated mechanism: those files are either merged
incorrectly into opencode.json (any JSON file) or must be wired through the separate home.yaml
manifest.
auth.json (credentials) lives in ~/.local/share/opencode/ and is already out of scope; it stays on
the existing home.yaml path.
We want the opencode/ directory to act as a verbatim mirror of the VM's ~/.config/opencode/
directory, while keeping the opencode.json snippet-merge behavior in an explicit, well-separated
subdirectory.
Goals
- Treat the top level of
opencode/ as a 1:1 verbatim mirror of ~/.config/opencode/ in the VM:
every file and subdirectory is provisioned verbatim, preserving its relative path.
- Keep deep-merging of
opencode.json snippets, but relocate it into a dedicated subdirectory
(opencode/config-snippets/) so it cannot collide with verbatim files.
- A top-level file named
opencode.json/opencode.jsonc/opencode.json5 remains special: it is
deep-merged (treated as a snippet), never copied verbatim. The merged output always wins over any
raw copy.
- Fix the latent bug where a non-
opencode.json JSON file (e.g. tui.json) at the top level was
wrongly merged into opencode.json.
- Document the resulting breaking change and provide a migration note.
Non-goals
- Provisioning
~/.local/share/opencode/ (e.g. auth.json). This remains the responsibility of the
existing home.yaml mechanism.
- Any write-back / sync of
auth.json from the VM to the host.
- A
share/ mirror directory.
- Interactive-login flows inside the VM.
Context (verified)
internal/opencodeconfig/opencodeconfig.go implements the snippet merge: scanSnippets reads every
json/jsonc/json5 file in the given dirs (user first, then project; alphabetical within each) and
deepMerges them into one map; BuildOpenCodeJSON(userDir, projectDir) returns the marshaled
opencode.json plus the ordered source list and a "has snippets" boolean.
internal/sandbox/reprovision/config_files.go (LoadConfigFiles) builds the desired VM state: it
calls config.BuildOpenCodeJSON(userConfigDir, projectOpenCodeDir) for the merged config and
homeconfig.BuildHomeFiles(...) for home.yaml files, returning a ConfigFiles struct
(HasSnippets, OpenCode, HomeFiles map[string][]byte, Hooks, Keys).
internal/sandbox/reprovision/reprovision.go writes the merged opencode.json to
/home/dev/.config/opencode/opencode.json (OpenCodeConfigPath(VMHomeDir)) and then each
HomeFiles entry to its VM path.
OpenCodeConfigPath (config_files.go:60) is the single reserved VM path for the merged config.
configpaths: UserOpencodeConfigDir() = ~/.config/opencode-sandbox/opencode;
ProjectOpencodeConfigDir() = .opencode-sandbox/opencode. Both currently feed the merge.
- opencode's
~/.config/opencode/ holds opencode.json (config), tui.json/tui.jsonc (TUI),
AGENTS.md (global rules), and extension subdirs agents/, commands/, plugins/, skills/,
themes/, tools/. It does not hold auth.json (that lives in ~/.local/share/opencode/).
Design
Source layout
Under ~/.config/opencode-sandbox/opencode/ (user) and .opencode-sandbox/opencode/ (project):
- Top level — verbatim mirror of
~/.config/opencode/. Every file and directory is provisioned
verbatim, preserving its relative path, except files named opencode.json, opencode.jsonc, or
opencode.json5 (the opencode.json(|c|5) exception pattern).
opencode/config-snippets/ — dedicated snippet subdirectory, excluded from the verbatim
copy. Files named opencode.json/opencode.jsonc/opencode.json5 here are deep-merged into the
final opencode.json.
Merge logic (final opencode.json)
The merged opencode.json is the deep merge of:
opencode/config-snippets/*opencode.json(c|5)* — user dir merged first, then project dir; within
each dir alphabetical (unchanged merge order semantics).
- A top-level
opencode/opencode.json/.jsonc/.json5, if present — deep-merged into the same
result.
Merged wins over verbatim: the merged output is always what is provisioned at
~/.config/opencode/opencode.json; a raw verbatim opencode.json is never copied. A top-level
opencode.json "undergoes normal deep merging" — it is treated as another snippet. If a user also
places a file that would verbatim-copy to opencode.json, it is not copied; instead it is folded into
the merge.
Provisioning & precedence
~/.config/opencode/opencode.json remains reserved for the merged output only.
- Verbatim mirror files are provisioned alongside home files on startup (same path as
HomeFiles), with an explicit home.yaml target winning over the mirror for the same VM path
(explicit beats implicit).
config-snippets/ is never copied verbatim; only its opencode.json(c|5) files are merged.
Implementation approach
Modify internal/opencodeconfig to:
- Introduce a notion of the reserved exception pattern: only files whose basename is exactly
opencode.json, opencode.jsonc, or opencode.json5 (case-insensitive extension) participate in
the merge. This replaces today's "every json/jsonc/json5 file is a snippet" rule.
- Add a function to scan the verbatim mirror: walk the top-level
opencode/ dirs (user + project,
project overriding user per path), excluding the config-snippets/ subdir and the
opencode.json(|c|5) exception files, and return the set of verbatim file paths/content keyed by
VM path under ~/.config/opencode/.
Modify internal/sandbox/reprovision/config_files.go:
- Extend
ConfigFiles to carry the verbatim mirror files (either merged into HomeFiles or a new
field) and the reserved opencode.json handling.
- Update
LoadConfigFiles to (a) merge config-snippets/ + top-level opencode.json(c|5), and
(b) collect the verbatim mirror files.
reprovision.go already writes OpenCodeConfigPath and HomeFiles; the verbatim mirror files flow
through the same write path. Home-file comparison logic (OpenCodeConfigEqual / config comparison)
must account for the new files.
Secrets note
auth.json is out of scope (see Non-goals). No secret-warning logic is added in this change; the
verbatim mirror is a general-purpose copy mechanism. If a future need arises for detecting raw secrets
in provisioned credential files, it will be designed separately.
Breaking change (document)
- Today any top-level
opencode/*.json* file is merged into opencode.json. Under the new model the
top level is a verbatim mirror, so:
- Existing snippet files must move into
opencode/config-snippets/.
- A top-level
opencode.json(c|5) is now merged (treated as a snippet), not copied verbatim.
- A top-level non-
opencode.json JSON file (e.g. tui.json) is now copied verbatim instead of
being merged — this is the fix to the latent bug.
- Document the new layout and migration in README/docs and
CHANGELOG.md [Unreleased].
Testing
- Unit tests in
internal/opencodeconfig: merge of config-snippets/; top-level opencode.json
folded into the merge; non-opencode.json top-level JSON excluded from the merge; verbatim mirror
scan excluding config-snippets/ and the exception pattern.
- CLI test(s) in
cmd/opencode-sandbox/cli_*_test.go covering any new/affected flags or config
subcommands (e.g. config show output for the new layout).
config_compare_test.go / reprovision tests updated for the new merge and mirror semantics.
make check passes (fmt, lint, test).
Documentation & changelog
README.md and docs/configuration.md (Opencode configuration section): describe the new
config-snippets/ subdir, the top-level verbatim mirror, the opencode.json(|c|5) exception, and
the breaking-change migration.
CHANGELOG.md: add an [Unreleased] entry for the layout change.
Files touched (anticipated)
internal/opencodeconfig/opencodeconfig.go — merge rule + verbatim mirror scanner.
internal/opencodeconfig/opencodeconfig_test.go — tests.
internal/sandbox/reprovision/config_files.go — ConfigFiles/LoadConfigFiles for mirror files.
internal/sandbox/reprovision/config_files_test.go / coverage_test.go / config_compare_test.go
— tests.
cmd/opencode-sandbox/ — any CLI test coverage for config show etc.
README.md, docs/configuration.md, CHANGELOG.md — docs and changelog.
Motivation
Today the
opencode/snippet directories (~/.config/opencode-sandbox/opencode/and.opencode-sandbox/opencode/) serve a single purpose: every*.json/*.jsonc/*.json5file under them is deep-merged into oneopencode.jsonprovisioned at/home/dev/.config/opencode/opencode.jsonin the VM.This conflation is a latent bug and a limitation:
tui.json,AGENTS.md,agents/**,commands/**,plugins/**,themes/**) has no dedicated mechanism: those files are either merged incorrectly intoopencode.json(any JSON file) or must be wired through the separatehome.yamlmanifest.auth.json(credentials) lives in~/.local/share/opencode/and is already out of scope; it stays on the existinghome.yamlpath.Proposed behavior
Make the
opencode/directory a verbatim mirror of the VM's~/.config/opencode/directory, while keeping theopencode.jsonsnippet-merge behavior in an explicit, well-separated subdirectory.opencode/as a 1:1 verbatim mirror of~/.config/opencode/: every file and subdirectory is provisioned verbatim, preserving its relative path.opencode.jsonsnippets, but relocate it into a dedicated subdirectory (opencode/config-snippets/) so it cannot collide with verbatim files.opencode.json/opencode.jsonc/opencode.json5remains special: it is deep-merged (treated as a snippet), never copied verbatim. The merged output always wins over any raw copy.opencode.jsonJSON file (e.g.tui.json) at the top level was wrongly merged intoopencode.json.Non-goals
~/.local/share/opencode/(e.g.auth.json) — stays on thehome.yamlmechanism.auth.jsonfrom the VM to the host.share/mirror directory.Alternatives considered
home.yamlmanifest for non-opencode.jsonfiles — works but adds per-file wiring and doesn't fix the merge-collision bug for JSON files at the top level.share/mirror alongsideconfig/— rejected as out of scope (see non-goals).Design spec
Full design spec (2026-08-28-opencode-config-mirror-design.md)
Design: opencode config directory mirror + verbatim provisioning
Date: 2026-08-28
Status: Draft (pending review)
Problem
Today the
opencode/snippet directories (~/.config/opencode-sandbox/opencode/and.opencode-sandbox/opencode/) serve a single purpose: every*.json/*.jsonc/*.json5file under them isdeep-merged into one
opencode.jsonprovisioned at/home/dev/.config/opencode/opencode.jsonin the VM.This conflation is a latent bug and a limitation:
tui.json,AGENTS.md,agents/**,commands/**,plugins/**,themes/**) has no dedicated mechanism: those files are either mergedincorrectly into
opencode.json(any JSON file) or must be wired through the separatehome.yamlmanifest.
auth.json(credentials) lives in~/.local/share/opencode/and is already out of scope; it stays onthe existing
home.yamlpath.We want the
opencode/directory to act as a verbatim mirror of the VM's~/.config/opencode/directory, while keeping the
opencode.jsonsnippet-merge behavior in an explicit, well-separatedsubdirectory.
Goals
opencode/as a 1:1 verbatim mirror of~/.config/opencode/in the VM:every file and subdirectory is provisioned verbatim, preserving its relative path.
opencode.jsonsnippets, but relocate it into a dedicated subdirectory(
opencode/config-snippets/) so it cannot collide with verbatim files.opencode.json/opencode.jsonc/opencode.json5remains special: it isdeep-merged (treated as a snippet), never copied verbatim. The merged output always wins over any
raw copy.
opencode.jsonJSON file (e.g.tui.json) at the top level waswrongly merged into
opencode.json.Non-goals
~/.local/share/opencode/(e.g.auth.json). This remains the responsibility of theexisting
home.yamlmechanism.auth.jsonfrom the VM to the host.share/mirror directory.Context (verified)
internal/opencodeconfig/opencodeconfig.goimplements the snippet merge:scanSnippetsreads everyjson/jsonc/json5 file in the given dirs (user first, then project; alphabetical within each) and
deepMerges them into one map;BuildOpenCodeJSON(userDir, projectDir)returns the marshaledopencode.jsonplus the ordered source list and a "has snippets" boolean.internal/sandbox/reprovision/config_files.go(LoadConfigFiles) builds the desired VM state: itcalls
config.BuildOpenCodeJSON(userConfigDir, projectOpenCodeDir)for the merged config andhomeconfig.BuildHomeFiles(...)forhome.yamlfiles, returning aConfigFilesstruct(
HasSnippets,OpenCode,HomeFiles map[string][]byte,Hooks,Keys).internal/sandbox/reprovision/reprovision.gowrites the mergedopencode.jsonto/home/dev/.config/opencode/opencode.json(OpenCodeConfigPath(VMHomeDir)) and then eachHomeFilesentry to its VM path.OpenCodeConfigPath(config_files.go:60) is the single reserved VM path for the merged config.configpaths:UserOpencodeConfigDir()=~/.config/opencode-sandbox/opencode;ProjectOpencodeConfigDir()=.opencode-sandbox/opencode. Both currently feed the merge.~/.config/opencode/holdsopencode.json(config),tui.json/tui.jsonc(TUI),AGENTS.md(global rules), and extension subdirsagents/,commands/,plugins/,skills/,themes/,tools/. It does not holdauth.json(that lives in~/.local/share/opencode/).Design
Source layout
Under
~/.config/opencode-sandbox/opencode/(user) and.opencode-sandbox/opencode/(project):~/.config/opencode/. Every file and directory is provisionedverbatim, preserving its relative path, except files named
opencode.json,opencode.jsonc, oropencode.json5(theopencode.json(|c|5)exception pattern).opencode/config-snippets/— dedicated snippet subdirectory, excluded from the verbatimcopy. Files named
opencode.json/opencode.jsonc/opencode.json5here are deep-merged into thefinal
opencode.json.Merge logic (final
opencode.json)The merged
opencode.jsonis the deep merge of:opencode/config-snippets/*opencode.json(c|5)*— user dir merged first, then project dir; withineach dir alphabetical (unchanged merge order semantics).
opencode/opencode.json/.jsonc/.json5, if present — deep-merged into the sameresult.
Merged wins over verbatim: the merged output is always what is provisioned at
~/.config/opencode/opencode.json; a raw verbatimopencode.jsonis never copied. A top-levelopencode.json"undergoes normal deep merging" — it is treated as another snippet. If a user alsoplaces a file that would verbatim-copy to
opencode.json, it is not copied; instead it is folded intothe merge.
Provisioning & precedence
~/.config/opencode/opencode.jsonremains reserved for the merged output only.HomeFiles), with an explicithome.yamltarget winning over the mirror for the same VM path(explicit beats implicit).
config-snippets/is never copied verbatim; only itsopencode.json(c|5)files are merged.Implementation approach
Modify
internal/opencodeconfigto:opencode.json,opencode.jsonc, oropencode.json5(case-insensitive extension) participate inthe merge. This replaces today's "every json/jsonc/json5 file is a snippet" rule.
opencode/dirs (user + project,project overriding user per path), excluding the
config-snippets/subdir and theopencode.json(|c|5)exception files, and return the set of verbatim file paths/content keyed byVM path under
~/.config/opencode/.Modify
internal/sandbox/reprovision/config_files.go:ConfigFilesto carry the verbatim mirror files (either merged intoHomeFilesor a newfield) and the reserved
opencode.jsonhandling.LoadConfigFilesto (a) mergeconfig-snippets/+ top-levelopencode.json(c|5), and(b) collect the verbatim mirror files.
reprovision.goalready writesOpenCodeConfigPathandHomeFiles; the verbatim mirror files flowthrough the same write path. Home-file comparison logic (
OpenCodeConfigEqual/ config comparison)must account for the new files.
Secrets note
auth.jsonis out of scope (see Non-goals). No secret-warning logic is added in this change; theverbatim mirror is a general-purpose copy mechanism. If a future need arises for detecting raw secrets
in provisioned credential files, it will be designed separately.
Breaking change (document)
opencode/*.json*file is merged intoopencode.json. Under the new model thetop level is a verbatim mirror, so:
opencode/config-snippets/.opencode.json(c|5)is now merged (treated as a snippet), not copied verbatim.opencode.jsonJSON file (e.g.tui.json) is now copied verbatim instead ofbeing merged — this is the fix to the latent bug.
CHANGELOG.md[Unreleased].Testing
internal/opencodeconfig: merge ofconfig-snippets/; top-levelopencode.jsonfolded into the merge; non-
opencode.jsontop-level JSON excluded from the merge; verbatim mirrorscan excluding
config-snippets/and the exception pattern.cmd/opencode-sandbox/cli_*_test.gocovering any new/affected flags orconfigsubcommands (e.g.
config showoutput for the new layout).config_compare_test.go/reprovisiontests updated for the new merge and mirror semantics.make checkpasses (fmt, lint, test).Documentation & changelog
README.mdanddocs/configuration.md(Opencode configuration section): describe the newconfig-snippets/subdir, the top-level verbatim mirror, theopencode.json(|c|5)exception, andthe breaking-change migration.
CHANGELOG.md: add an[Unreleased]entry for the layout change.Files touched (anticipated)
internal/opencodeconfig/opencodeconfig.go— merge rule + verbatim mirror scanner.internal/opencodeconfig/opencodeconfig_test.go— tests.internal/sandbox/reprovision/config_files.go—ConfigFiles/LoadConfigFilesfor mirror files.internal/sandbox/reprovision/config_files_test.go/coverage_test.go/config_compare_test.go— tests.
cmd/opencode-sandbox/— any CLI test coverage forconfig showetc.README.md,docs/configuration.md,CHANGELOG.md— docs and changelog.