#9095 · @LeonMueller-OneAndOnly · opened Jan 17, 2026 at 3:52 PM UTC · last updated Mar 21, 2026 at 12:50 PM UTC

fix(TUI): complete auth fix for TUI with server password (HTTP mode)

tuifix
86
+41155 files

Score breakdown

Impact

9.0

Clarity

9.0

Urgency

8.0

Ease Of Review

9.0

Guidelines

9.0

Readiness

9.0

Size

7.0

Trust

8.0

Traction

9.0

Summary

This PR completes a TUI authentication fix for HTTP server mode when OPENCODE_SERVER_PASSWORD is set. It addresses a bug where the TUI itself couldn't authenticate to its spawned server, fixing several related issues.

Open in GitHub

Description

Summary

Completes the TUI authentication fix for when OPENCODE_SERVER_PASSWORD is set and TUI starts an HTTP server, due to the --port or '--hostname' flag being set.

Problem

PR #8179 fixed TUI authentication when using direct RPC communication, but didn't handle the case where TUI starts an HTTP server (when --port flag is used). In this scenario, the TUI would fail to authenticate against its own server.

Solution

  • Extracted getAuthorizationHeader() to a shared module packages/opencode/src/flag/auth.ts

  • Modified packages/opencode/src/cli/cmd/tui/thread.ts to use a custom fetch function that includes the Authorization header when an HTTP server is started with password protection

  • Modified packages/opencode/src/cli/cmd/tui/attach.ts to use a custom fetch function too (if needed)

  • Modified packages/opencode/src/plugin/index.ts to use a custom fetch function too (if needed)

  • The fix ensures the TUI can authenticate to the HTTP server it spawns when password is set. For plugins, the base TUI command itself and the attach TUI command.

Testing [Base TUI command]

Verified with:

OPENCODE_SERVER_PASSWORD="test" bun dev . --hostname 127.0.0.1 --port 4096

Previously this would fail with Unauthorized errors. Now the TUI successfully authenticates and works as expected.

Testing [Attach command]

Terminal 1 [start server process]: OPENCODE_SERVER_PASSWORD="test" bun dev serve --hostname 127.0.0.1 --port 4096

Terminal 2 [attach TUI to existing server]: OPENCODE_SERVER_PASSWORD="test" bun dev attach http://127.0.0.1:4096/

Previously this would fail with Unauthorized errors. Now the TUI successfully authenticates and works as expected.

Related

  • Completes #8179 (previous partial fix)
  • Fixes #8173 (original issue)
  • Fixes #9066 (same root issue)
  • Fixes #8676 (duplicate issue)
  • Fixes #8458 (same root cause)
  • Fixes #10166 (duplicate issue)

This PR completes the authentication flow that was partially addressed in #8179. The previous PR only fixed the RPC communication path, while this PR fixes the HTTP server path.

Linked Issues

#8676 fix: Plugin client returns 401 Unauthorized when OPENCODE_SERVER_PASSWORD is set (Desktop)

View issue

#10166 OPENCODE_SERVER_PASSWORD设置时,远程启动opencode server,desktop无法连接。

View issue

#8173 OPENCODE_SERVER_PASSWORD in env prevents user from working in TUI

View issue

#8458 [FEATURE]: Attach to authenticated OC Server

View issue

#9066 Failing to authenticate into web UI when username or password is set

View issue

Comments

PR comments

R44VC0RP

Heads up - #9706 was just opened which identifies the same auth header issue but specifically for the plugin client in plugin/index.ts.

This PR fixes the TUI HTTP mode auth, but the plugin client at packages/opencode/src/plugin/index.ts:24-28 also needs the same treatment - it creates a client without auth headers.

Could we extend this PR to also add auth headers to the plugin client fetch wrapper? The fix would be similar to what you've done for the TUI worker.

LeonMueller-OneAndOnly

@R44VC0RP This PR now also includes the same authorization-header fix for the plugin client in packages/opencode/src/plugin/index.ts 👍

0xRichardH

Hi @LeonMueller-OneAndOnly

Great work on this PR! I tested it with a remote server and found that the opencode attach command also needs the same authentication fix.

The attach command in packages/opencode/src/cli/cmd/tui/attach.ts doesn't pass authentication headers when connecting to a remote server with OPENCODE_SERVER_PASSWORD set. Here's the fix (you can also apply this patch):

diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts
index 3f9285f63..3390a03f7 100644
--- a/packages/opencode/src/cli/cmd/tui/attach.ts
+++ b/packages/opencode/src/cli/cmd/tui/attach.ts
@@ -1,5 +1,6 @@
 import { cmd } from "../cmd"
 import { tui } from "./app"
+import { getAuthorizationHeader } from "../../../flag/auth"
 
 export const AttachCommand = cmd({
   command: "attach <url>",
@@ -22,10 +23,22 @@ export const AttachCommand = cmd({
       }),
   handler: async (args) => {
     if (args.dir) process.chdir(args.dir)
+
+    // If server requires authentication, create a custom fetch that includes the auth header
+    const authHeader = getAuthorizationHeader()
+    const customFetch = authHeader
+      ? ((async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
+          const request = new Request(input, init)
+          request.headers.set("Authorization", authHeader)
+          return fetch(request)
+        }) as typeof fetch)
+      : undefined
+
     await tui({
       url: args.url,
       args: { sessionID: args.session },
       directory: args.dir ? process.cwd() : undefined,
+      fetch: customFetch,
     })
   },
 })

Tested with:

export OPENCODE_SERVER_USERNAME=myuser
export OPENCODE_SERVER_PASSWORD=mypass
opencode attach https://my-remote-server.example.com

This completes the authentication fix for all TUI connection modes.

<img width="1778" height="1402" alt="image" src="https://github.com/user-attachments/assets/55586c7a-8153-4227-b160-71ddb968e1a4" />

LeonMueller-OneAndOnly

@0xRichardH Thank you, that makes sense. I added another commit that resembles your patch to this PR

LeonMueller-OneAndOnly

@0xRichardH I have reverted the changes done by this patch, since the windows-test (the github action) is not passing anymore after applying it. Not sure if this is a reproducible issue and/or a flaky windows test build. Sadly I have no windows computer to investigate. A separate pull request would be good for this in my opinion.

LeonMueller-OneAndOnly

Okay, even after revertig the windows check does not pass. Since it passed in the commit before, I am pretty sure this I either an error in the current dev branch and/or a flaky test.

Does a maintainer/collaborator have more insights regarding this?

LeonMueller-OneAndOnly

So final verdict from my side:

  • The patch for the attach command was now reapplied after previously reverting it

  • The underlying issue is present in the current dev branch

Here is a screenshot as of now to display that the windows-check is failing on the dev-branch itself currently.

<img width="1272" height="545" alt="Bildschirmfoto 2026-01-21 um 10 08 00" src="https://github.com/user-attachments/assets/e323a0fc-3ce1-4716-953e-1b6f6e6aaf4b" />

Sly777

Hey @R44VC0RP do you have any plan about this PR?

Florosonic

I have this promlem in my Opencode and wait PR.

alexellis

I have the same issue (testing with SlicerVM.com) - the idea is to run a microVM, and opencode with the repo inside.. isolated. And then to run opencode as a thin client locally for control.

This PR implies that the issue is only present when --port is given, but I get it regardless?

# SlicerVM:

OPENCODE_SERVER_PASSWORD=secret opencode web --hostname 0.0.0.0

# Mac
OPENCODE_PASSWORD=secret OPENCODE_USER=opencode opencode attach http://192.168.141.2:4096                                                                              
Unauthorized

Workaround for now is to turn auth off, but port-forwarding to a local port on the Mac means any process could potentially gain access.

Will keep an eye on this, just wanted to clarify whether I misunderstood: "Completes the TUI authentication fix for when OPENCODE_SERVER_PASSWORD is set and TUI starts an HTTP server, due to the --port flag being set."

Since this may be using basic auth, have you also considered parsing the username/password from the URL to avoid clunky env-vars (at least as a second option?

For instance:

opencode attach http://opencode:secret@192.168.141.2:4096                                                                              

# Or:

ENV OC_URL="http://opencode:secret@192.168.141.2:4096"
opencode attach   

LeonMueller-OneAndOnly

Since this may be using basic auth, have you also considered parsing the username/password from the URL to avoid clunky env-vars (at least as a second option?

For instance:

This PR only aims to fix the existing issues when using the existing SERVER_PASSWORD / USER_NAME env-variables for plugins, the TUI+server starting and TUI attaching.

Adding functionality for username + password via URL-parameters is not in scope of this PR - feel free however to open another PR that builds on this. I think that is a good idea.

imarshallwidjaja

can devs merge this fix please ❤️

LeonMueller-OneAndOnly

This PR implies that the issue is only present when --port is given, but I get it regardless?

The issue is present when OpenCode starts an HTTP server alongside the TUI. This happens when either the port or hostname are provided as CLI arguments. When using the opencode web command, this is always the case.

The basic TUI regularly starts another process to run the server and communicates over RPC with this. For this case the auth issue was already fixed by PR #8179

HTTP is only chosen as a communication protocol when needed for Web usage.

rekram1-node

/review

kevincojean

It would be great for this PR to be merged, securing access to the local server is important!

Sly777

Is there any way to use this feature as a plugin or something? Because it's not getting merged somehow.

SrHenry

Is there any way to use this feature as a plugin or something? Because it's not getting merged somehow.

You can clone & checkout the latest release, add the PR owner's remote and merge his PR branch to the release and run it locally in dev mode, then you keep merging or rebasing the subsequent releases as they come while this PR isn't merged yet to dev and released.

LeonMueller-OneAndOnly

Is there any way to use this feature as a plugin or something? Because it's not getting merged somehow.

Building directly from a fork is the only feasible solution for that. A fix like this isn't what a plugin can or should do.

TheOrdinaryWow

It would be nice to support auth in desktop as well.

Currently, use the base auth style url is not permitted.

http://username:password@example.com:port

This would be simplistic method of connecting to a remote server instead of popup (that works as a workaround).

LeonMueller-OneAndOnly

It would be nice to support auth in desktop as well.

Currently, use the base auth style url is not permitted.

http://username:password@example.com:port

This would be simplistic method of connecting to a remote server instead of popup (that works as a workaround).

I agree that this is a sensible feature request. It is however out of scope of this PR. This one is already open for a long time, further extending the scope of this will make the review harder and therefore is not desirable imo.

Hicsy

will be great if this feature can make it in to one of the recent releases that have come out since.

dhowe

status on this?

Changed Files

packages/opencode/src/cli/cmd/tui/attach.ts

+94
@@ -5,6 +5,7 @@ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { TuiConfig } from "@/config/tui"
import { Instance } from "@/project/instance"
import { existsSync } from "fs"
import { getAuthorizationHeader } from "@/flag/auth"
export const AttachCommand = cmd({
command: "attach <url>",
@@ -38,6 +39,11 @@ export const AttachCommand = cmd({
alias: ["p"],
type: "string",
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
})
.option("username", {
alias: ["u"],
type: "string",
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
}),
handler: async (args) => {
const unguard = win32InstallCtrlCGuard()
@@ -61,10 +67,9 @@ export const AttachCommand = cmd({
}
})()
const headers = (() => {
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
if (!password) return undefined
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
return { Authorization: auth }
const Authorization = getAuthorizationHeader({

packages/opencode/src/cli/cmd/tui/thread.ts

+121
@@ -14,6 +14,7 @@ import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { TuiConfig } from "@/config/tui"
import { Instance } from "@/project/instance"
import { getAuthorizationHeader } from "@/flag/auth"
declare global {
const OPENCODE_WORKER_PATH: string
@@ -25,6 +26,7 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const request = new Request(input, init)
const body = request.body ? await request.text() : undefined
const result = await client.call("fetch", {
url: request.url,
method: request.method,
@@ -182,7 +184,16 @@ export const TuiThreadCommand = cmd({
const transport = external
? {
url: (await client.call("server", network)).url,
fetch: undefined,
fetch: (() => {
const authHeader = getAuthorizationHeader()
if (!authHeader) return undefined
return (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const req

packages/opencode/src/cli/cmd/tui/worker.ts

+18
@@ -9,7 +9,7 @@ import { Config } from "@/config/config"
import { GlobalBus } from "@/bus/global"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
import type { BunWebSocketData } from "hono/bun"
import { Flag } from "@/flag/flag"
import { getAuthorizationHeader } from "@/flag/auth"
import { setTimeout as sleep } from "node:timers/promises"
await Log.init({
@@ -144,10 +144,3 @@ export const rpc = {
}
Rpc.listen(rpc)
function getAuthorizationHeader(): string | undefined {
const password = Flag.OPENCODE_SERVER_PASSWORD
if (!password) return undefined
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
return `Basic ${btoa(`${username}:${password}`)}`
}

packages/opencode/src/flag/auth.ts

+110
@@ -0,0 +1,11 @@
import { Flag } from "@/flag/flag"
export function getAuthorizationHeader(options?: {
passwordFromCli?: string
usernameFromCli?: string
}): string | undefined {
const password = options?.passwordFromCli ?? Flag.OPENCODE_SERVER_PASSWORD
if (!password) return undefined
const username = options?.usernameFromCli ?? Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
return `Basic ${btoa(`${username}:${password}`)}`
}

packages/opencode/src/plugin/index.ts

+82
@@ -7,6 +7,7 @@ import { Server } from "../server/server"
import { BunProc } from "../bun"
import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
import { getAuthorizationHeader } from "../flag/auth"
import { CodexAuthPlugin } from "./codex"
import { Session } from "../session"
import { NamedError } from "@opencode-ai/util/error"
@@ -22,11 +23,16 @@ export namespace Plugin {
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin]
const state = Instance.state(async () => {
const authHeader = getAuthorizationHeader()
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
directory: Instance.directory,
// @ts-ignore - fetch type incompatibility
fetch: async (...args) => Server.App().fetch(...args),
fetch: (async (input, init) => {
const request = new Request(input, init)
if (authHeader) request.headers.set("Authorization", authHeader)
return Server.App().fetch(request)
}) as typeof fetch,
})
const config = await Config.get()
const hooks: Hooks[] = []