Skip to content

OpenCode integration dead-ends in permanent 'conflict' after any unrelated user edit to opencode.json (e.g. adding an MCP server) #1631

Closed

Description

@robin-bially

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 conflict when the owned block is intact (mirrors the existing restore --confirm-drift pattern). If recordedFragmentFingerprint(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)), classify current/stale instead of conflict — rewriting such a file provably cannot destroy formatting or comments. Files with custom formatting/comments still fail closed and would use path A.

Reproduction

  1. ocx integration client enable --client opencode → state current.
  2. Add any MCP server (or any other key) to ~/.config/opencode/opencode.json without touching provider.opencodex, e.g.:
    "mcp": { "playwright": { "type": "local", "command": ["npx", "-y", "@playwright/mcp@latest"], "enabled": true } }
  3. Dashboard → Integrations → OpenCode now shows Konflikt, toggle off.
  4. ocx integration client enable --client opencode refuses with <path> changed after opencodex wrote it.
  5. 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.opencodex block 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: conflict

Screenshots 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

  1. coderabbitai commented on Aug 13, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #892 - fix: bound the six remaining retained-state leaks (#820 wave 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 plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. github-actions commented on Aug 13, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  3. robin-bially commented on Aug 13, 2026

    @robin-bially
    ContributorAuthor

    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.json as strict JSON — a file carrying comments never reaches the conflict branch at all (it fails parsing and classifies unsafe). So for format: "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 but recordedFragmentFingerprint(parsed, record) === record.blockFingerprint, classify stale (re-appliable) instead of conflict — 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.

  4. lidge-jun commented on Aug 13, 2026

    @lidge-jun
    Owner

    Confirmed against current dev (040f6db5b); the report reproduces from code reading and is not fixed on dev yet.

    src/integrations/state.ts:183-185 returns conflict: foreign-edit whenever a non-OMP file fingerprint differs from its record. The owned-fragment check that could prove providers.opencodex is intact appears only afterwards at src/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 stale only 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 dev with completed CI.

    DISPOSITION: ACCEPTED

  5. added a commit that references this issue on Aug 14, 2026
    30c1709
  6. Wibias commented on Aug 14, 2026

    @Wibias
    Contributor

    Fixed by #1632

  7. added a commit that references this issue on Sep 17, 2026
    d152b92
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions