Skip to content

[FEATURE] Desktop: persist sidecar server URL/credentials to disk (XDG_STATE_HOME) so companion tools can auto-discover #47206

Description

@fushoujiang

Description

The Desktop app's embedded server (sidecar) is only reachable by the app's own renderer. Companion tools running on the same machine (status-bar widgets, CLI wrappers, IDE extensions, automation scripts) cannot discover or authenticate to it, because the server URL/credentials are generated in memory and never persisted.

Currently the only working "API" for external tools is scraping main.log for the URL:

[info] server ready { url: 'http://127.0.0.1:62057' }

…but the log never contains the password, so discovery always ends in HTTP 401.

Current behavior (v1.18.26, macOS)

  • out/main/index.js: const password = randomUUID(); — a fresh password per launch, passed to the sidecar via postMessage, never written to disk.

  • out/main/sidecar.jsprepareSidecarEnv() unconditionally overwrites the environment:

    Object.assign(process.env, {
      OPENCODE_SERVER_USERNAME: "opencode",
      OPENCODE_SERVER_PASSWORD: password,   // always the IPC-provided value
      ...
    })

    so externally fixing the password (e.g. launchctl setenv OPENCODE_SERVER_PASSWORD ...) has no effect.

  • The password is only visible inside processes the server itself spawns (session tool processes). Tools outside that process tree cannot read it: ps -E shows the kernel-initial env, and the runtime-injected value is not there.

Result: every desktop restart rotates port + password, and each external tool has to implement a hack — e.g. running a "bridge" script inside an opencode session that dumps its own env to a well-known file — which only works after a session has started, and silently breaks again on the next restart.

Proposal

After server ready, persist the endpoint once, atomically, with 0600 perms, e.g.:

// $XDG_STATE_HOME/opencode/server.json  (sidecar already sets XDG_STATE_HOME)
{
  "url": "http://127.0.0.1:62057",
  "username": "opencode",
  "password": "<uuid>",
  "pid": 52431,
  "writtenAt": 1788490335685
}
  • Rewrite it on every (re)start; remove or invalidate it on clean shutdown.
  • Loopback-only listener + same-user-readable file puts it in the same trust domain as the existing env-var mechanism used by the CLI (OPENCODE_SERVER_PASSWORD), so no new attack surface.

(Alternative, also fine: let the desktop honor an externally provided OPENCODE_SERVER_PASSWORD instead of always generating one. Then tools can pin credentials via launchctl setenv and discover the port from main.log/lsof as they already do.)

Precedent inside the same codebase

The v2 CLI background-service path already treats credentials as externally retrievable:

const url = await run(binary, ["service", "start"], ...)
const password = await run(binary, ["service", "get", "password"], ...)

The desktop sidecar is currently the only server mode whose credentials are unreachable from outside the process tree.

Alternatives considered (and why they don't work today)

  1. A plugin that writes credentials on startup — natural place for it, but desktop plugin loading is broken/limited on 1.18.x (see [Bug] Production Desktop 1.18.15 still cannot load Bun-target plugins (oh-my-openagent); V2 sidecar not bundled #41033, Desktop app: local plugins load and register but their hooks (tool.execute.before, event) are never invoked #38604, [Desktop][Windows] npm plugin from config is silently never loaded — no install attempt logged, docs/cache mismatch #44367), so it can't be relied on.
  2. Scraping ps -E for OPENCODE_SERVER_PASSWORD — the value is injected at runtime, so it only appears in short-lived session tool processes; not a stable discovery channel.

Persisting the endpoint (or honoring an external password) would let the growing ecosystem of companion tools connect zero-touch, instead of every tool shipping its own rebridge hack.

Environment

  • OpenCode Desktop 1.18.26, macOS 15 (arm64)
  • Reproduction: start desktop → curl -u opencode:anything http://127.0.0.1:<sidecar-port>/global/health → 401, with no supported way to learn the real password from outside the app.

Activity

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