Repository navigation
OpenCode integration dead-ends in permanent 'conflict' after any unrelated user edit to opencode.json (e.g. adding an MCP server) #1631
Description
Client or integration
Other
Area
Other
Summary
The OpenCode (and any non-omp) client integration dead-ends in a permanent conflict state as soon as the user makes any edit to their own config file — even edits that never touch the opencodex-owned block. Adding an MCP server to ~/.config/opencode/opencode.json is enough: the dashboard shows Konflikt, the toggle turns off, and there is no non-destructive recovery path. The only UI option, restoring the snapshot, would revert the user's newer edits; the only clean fix is manual surgery (delete the provider.opencodex block by hand, then re-enable).
I expected: edits outside the opencodex-owned block (normal, frequent usage of one's own config — MCP servers, instructions, permissions) either don't trip the conflict state, or there is a supported, non-destructive way to re-apply/re-own from the dashboard or CLI.
Root cause — classifyIntegration (src/integrations/state.ts) checks two fingerprints:
if (clientId !== "omp" && fingerprint(input.fileText ?? "") !== input.record.fileFingerprint) {
return { state: "conflict", reason: "foreign-edit" }; // ← fires on ANY file edit
}
if (recordedFragmentFingerprint(input.parsed, input.record) !== input.record.blockFingerprint) {
return { state: "conflict", reason: "foreign-edit" }; // ← the actual tamper check
}The whole-file check fires even when the second check proves the owned block is untouched. I understand this is deliberate (the test "whole-document serializers still conflict on an unrelated source edit" pins it): non-omp clients rewrite the whole document on apply, so a rewrite could normalize a user's formatting or drop comments, and the classifier fails closed. But the practical effect is that the integration breaks permanently on the first config edit the user makes themselves, with no recovery path.
Proposal — two options, not mutually exclusive; I'm happy to send a PR with regression tests for whichever direction fits the project's invariants:
- A. Confirm-gated re-apply from
conflictwhen the owned block is intact (mirrors the existingrestore --confirm-driftpattern). IfrecordedFragmentFingerprint(parsed, record) === record.blockFingerprint, apply merges into the current parsed document and a snapshot is taken first, so no user content can be lost — only formatting may normalize. CLI:ocx integration client enable --client opencode --confirm-drift(or similar); dashboard: a confirm dialog instead of the dead-end badge. - B. Classifier refinement for lossless cases: when the owned block is intact and the file round-trips byte-identically through the client's serializer (
fingerprint(serializeDocument(parsed, format)) === fingerprint(fileText)), classifycurrent/staleinstead ofconflict— rewriting such a file provably cannot destroy formatting or comments. Files with custom formatting/comments still fail closed and would use path A.
Reproduction
ocx integration client enable --client opencode→ statecurrent.- Add any MCP server (or any other key) to
~/.config/opencode/opencode.jsonwithout touchingprovider.opencodex, e.g.:"mcp": { "playwright": { "type": "local", "command": ["npx", "-y", "@playwright/mcp@latest"], "enabled": true } }
- Dashboard → Integrations → OpenCode now shows
Konflikt, toggle off. ocx integration client enable --client opencoderefuses with<path> changed after opencodex wrote it.- No non-destructive way out: "restore this state" reverts the user's own edit from step 2; the only clean recovery is deleting the
provider.opencodexblock manually and enabling again.
Version
2.14.2
Operating system
macOS 27 (arm64)
Provider and model
No response
Logs or error output
$ ocx integration client enable --client opencode
refused: /Users/<user>/.config/opencode/opencode.json changed after opencodex wrote it (conflict)
$ ocx integration client status --client opencode
clientId: opencode
state: conflictScreenshots and supporting files
No response
Redacted configuration
No response
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
coderabbitai commentedon Aug 13, 2026
🔗 Related PRs
#892 - fix: bound the six remaining retained-state leaks (
#820wave 2) [merged]#998 - feat(codex): a write substrate that survives being interrupted [merged]
#1106 - feat(toggles): Codex restore truth, Claude Desktop switch, composed acceptance (WP13 resume) [merged]
#1365 - fix(omp): preserve rich native model capabilities [open]
#1392 - fix(omp): follow up source-preservation hardening for
#1365[merged]📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
github-actions commentedon Aug 13, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
robin-bially commentedon Aug 13, 2026
Refining the proposal after more thought on why the fail-closed whole-file check exists (comment/formatting loss on whole-document rewrite):
For comment-incapable formats the protection is empty. The OpenCode client parses
opencode.jsonas strict JSON — a file carrying comments never reaches the conflict branch at all (it fails parsing and classifiesunsafe). So forformat: "json"clients, a re-apply with an intact owned block provably cannot lose user content (apply merges into the current parsed document), and the only possible change outside the owned block is formatting normalization. Weighed against the current behavior — the integration permanently dead-ends on the user's first own config edit — that trade-off seems clearly right.Concretely: in
classifyIntegration, when the whole-file fingerprint mismatches butrecordedFragmentFingerprint(parsed, record) === record.blockFingerprint, classifystale(re-appliable) instead ofconflict— but only for clients whose config format cannot carry comments (json). YAML / JSON5 / TOML clients keep today's hard conflict.I'll send a PR with this change plus regression tests shortly, so there's something concrete to review; happy to rework it toward the confirm-gated variant (A) instead if you prefer that direction.
lidge-jun commentedon Aug 13, 2026
Confirmed against current
dev(040f6db5b); the report reproduces from code reading and is not fixed ondevyet.src/integrations/state.ts:183-185returnsconflict: foreign-editwhenever a non-OMP file fingerprint differs from its record. The owned-fragment check that could proveproviders.opencodexis intact appears only afterwards atsrc/integrations/state.ts:186-188, so it is unreachable for any unrelated OpenCode edit. OpenCode is a strict-JSON whole-document integration (src/clients/config-export.ts:1073-1085), which makes the reported MCP-server edit sufficient to put the integration into the permanent conflict path.Open PR #1632 is the appropriate fix: it moves the owned-fragment verification first and returns
staleonly for strict JSON with intact owned fragments, while preserving conflicts for comment-capable whole-document formats and for edits inside the owned block. Its writer-level regressions cover both re-apply preservation and disable preservation.This issue will be closed once #1632 lands on
devwith completed CI.DISPOSITION: ACCEPTED
fix(integrations): classify a json sibling edit as stale instead of c…
Wibias commentedon Aug 14, 2026
Fixed by #1632
fix(integrations): classify a json sibling edit as stale instead of c…