From 35405a3c4ac17a6e00b24b6c953a6597f75185a7 Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Wed, 4 Mar 2026 22:26:41 -0600 Subject: [PATCH 1/5] fix(opencode): pass auth headers in `run --attach` Fixes #16096 Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/run.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 61bc609bb7c..df2e5d3772f 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -648,7 +648,13 @@ export const RunCommand = cmd({ } if (args.attach) { - const sdk = createOpencodeClient({ baseUrl: args.attach, directory }) + const headers = (() => { + const password = Flag.OPENCODE_SERVER_PASSWORD + if (!password) return undefined + const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" + return { Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` } + })() + const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers }) return await execute(sdk) } From 22c2dbfeed5afa7d591b888e52d24a9c81c2880b Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Wed, 4 Mar 2026 22:28:12 -0600 Subject: [PATCH 2/5] test: add tests for getAttachHeaders auth logic Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/run.ts | 13 +++++---- .../opencode/test/cli/run-attach-auth.test.ts | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 packages/opencode/test/cli/run-attach-auth.test.ts diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index df2e5d3772f..ca550932101 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -648,12 +648,7 @@ export const RunCommand = cmd({ } if (args.attach) { - const headers = (() => { - const password = Flag.OPENCODE_SERVER_PASSWORD - if (!password) return undefined - const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" - return { Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` } - })() + const headers = getAttachHeaders(Flag.OPENCODE_SERVER_PASSWORD, Flag.OPENCODE_SERVER_USERNAME) const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers }) return await execute(sdk) } @@ -668,3 +663,9 @@ export const RunCommand = cmd({ }) }, }) + +export function getAttachHeaders(password?: string, username?: string) { + if (!password) return undefined + const auth = `Basic ${Buffer.from(`${username ?? "opencode"}:${password}`).toString("base64")}` + return { Authorization: auth } +} diff --git a/packages/opencode/test/cli/run-attach-auth.test.ts b/packages/opencode/test/cli/run-attach-auth.test.ts new file mode 100644 index 00000000000..b82f446c487 --- /dev/null +++ b/packages/opencode/test/cli/run-attach-auth.test.ts @@ -0,0 +1,29 @@ +import { test, expect, describe } from "bun:test" +import { getAttachHeaders } from "../../src/cli/cmd/run" + +describe("getAttachHeaders", () => { + test("returns auth headers when password is set", () => { + const headers = getAttachHeaders("secret") + expect(headers).toEqual({ + Authorization: `Basic ${Buffer.from("opencode:secret").toString("base64")}`, + }) + }) + + test("uses custom username when provided", () => { + const headers = getAttachHeaders("secret", "admin") + expect(headers).toEqual({ + Authorization: `Basic ${Buffer.from("admin:secret").toString("base64")}`, + }) + }) + + test("defaults username to opencode", () => { + const headers = getAttachHeaders("secret") + const decoded = atob(headers!.Authorization.replace("Basic ", "")) + expect(decoded).toBe("opencode:secret") + }) + + test("returns undefined when no password", () => { + expect(getAttachHeaders(undefined)).toBeUndefined() + expect(getAttachHeaders("")).toBeUndefined() + }) +}) From aa38e055cc9328fdd1a02fc51e7489ce1b247ce1 Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Wed, 4 Mar 2026 22:36:47 -0600 Subject: [PATCH 3/5] refactor: extract shared getAuthorizationHeader to src/util/auth Consolidates auth header logic from worker.ts and attach.ts into a shared util used by run.ts, attach.ts, and worker.ts. Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/run.ts | 10 ++----- packages/opencode/src/cli/cmd/tui/attach.ts | 6 ++-- packages/opencode/src/cli/cmd/tui/worker.ts | 7 +---- packages/opencode/src/util/auth.ts | 9 ++++++ .../opencode/test/cli/run-attach-auth.test.ts | 30 ++++++------------- 5 files changed, 25 insertions(+), 37 deletions(-) create mode 100644 packages/opencode/src/util/auth.ts diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index ca550932101..b88a39bb787 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -7,6 +7,7 @@ import { Flag } from "../../flag/flag" import { bootstrap } from "../bootstrap" import { EOL } from "os" import { Filesystem } from "../../util/filesystem" +import { getAuthorizationHeader } from "../../util/auth" import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" import { Provider } from "../../provider/provider" @@ -648,7 +649,8 @@ export const RunCommand = cmd({ } if (args.attach) { - const headers = getAttachHeaders(Flag.OPENCODE_SERVER_PASSWORD, Flag.OPENCODE_SERVER_USERNAME) + const auth = getAuthorizationHeader() + const headers = auth ? { Authorization: auth } : undefined const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers }) return await execute(sdk) } @@ -663,9 +665,3 @@ export const RunCommand = cmd({ }) }, }) - -export function getAttachHeaders(password?: string, username?: string) { - if (!password) return undefined - const auth = `Basic ${Buffer.from(`${username ?? "opencode"}:${password}`).toString("base64")}` - return { Authorization: auth } -} diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index e892f9922d1..7d8b9e4f229 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -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 "@/util/auth" export const AttachCommand = cmd({ command: "attach ", @@ -61,9 +62,8 @@ 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")}` + const auth = getAuthorizationHeader(args.password) + if (!auth) return undefined return { Authorization: auth } })() const config = await Instance.provide({ diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index e63f10ba80c..4737aaa6141 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -10,6 +10,7 @@ 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 "@/util/auth" await Log.init({ print: process.argv.includes("--print-logs"), @@ -144,9 +145,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}`)}` -} diff --git a/packages/opencode/src/util/auth.ts b/packages/opencode/src/util/auth.ts new file mode 100644 index 00000000000..1ec44431ce4 --- /dev/null +++ b/packages/opencode/src/util/auth.ts @@ -0,0 +1,9 @@ +import { Flag } from "../flag/flag" + +export function getAuthorizationHeader( + password = Flag.OPENCODE_SERVER_PASSWORD, + username = Flag.OPENCODE_SERVER_USERNAME, +) { + if (!password) return undefined + return `Basic ${btoa(`${username ?? "opencode"}:${password}`)}` +} diff --git a/packages/opencode/test/cli/run-attach-auth.test.ts b/packages/opencode/test/cli/run-attach-auth.test.ts index b82f446c487..a6577da0d1d 100644 --- a/packages/opencode/test/cli/run-attach-auth.test.ts +++ b/packages/opencode/test/cli/run-attach-auth.test.ts @@ -1,29 +1,17 @@ import { test, expect, describe } from "bun:test" -import { getAttachHeaders } from "../../src/cli/cmd/run" +import { getAuthorizationHeader } from "../../src/util/auth" -describe("getAttachHeaders", () => { - test("returns auth headers when password is set", () => { - const headers = getAttachHeaders("secret") - expect(headers).toEqual({ - Authorization: `Basic ${Buffer.from("opencode:secret").toString("base64")}`, - }) - }) - - test("uses custom username when provided", () => { - const headers = getAttachHeaders("secret", "admin") - expect(headers).toEqual({ - Authorization: `Basic ${Buffer.from("admin:secret").toString("base64")}`, - }) +describe("getAuthorizationHeader", () => { + test("returns undefined when no password", () => { + expect(getAuthorizationHeader(undefined)).toBeUndefined() + expect(getAuthorizationHeader("")).toBeUndefined() }) - test("defaults username to opencode", () => { - const headers = getAttachHeaders("secret") - const decoded = atob(headers!.Authorization.replace("Basic ", "")) - expect(decoded).toBe("opencode:secret") + test("returns basic auth with default username", () => { + expect(getAuthorizationHeader("secret")).toBe(`Basic ${btoa("opencode:secret")}`) }) - test("returns undefined when no password", () => { - expect(getAttachHeaders(undefined)).toBeUndefined() - expect(getAttachHeaders("")).toBeUndefined() + test("uses custom username when provided", () => { + expect(getAuthorizationHeader("secret", "admin")).toBe(`Basic ${btoa("admin:secret")}`) }) }) From 76796793135cdc8894f2d2cdb47e9b9ba88683d1 Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Wed, 4 Mar 2026 22:42:47 -0600 Subject: [PATCH 4/5] revert: remove extracted util, inline auth pattern in run.ts Match existing inline pattern from attach.ts instead of extracting. Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/run.ts | 9 ++++++--- packages/opencode/src/cli/cmd/tui/attach.ts | 6 +++--- packages/opencode/src/cli/cmd/tui/worker.ts | 7 ++++++- packages/opencode/src/util/auth.ts | 9 --------- .../opencode/test/cli/run-attach-auth.test.ts | 17 ----------------- 5 files changed, 15 insertions(+), 33 deletions(-) delete mode 100644 packages/opencode/src/util/auth.ts delete mode 100644 packages/opencode/test/cli/run-attach-auth.test.ts diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index b88a39bb787..76f08b9d8ab 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -7,7 +7,6 @@ import { Flag } from "../../flag/flag" import { bootstrap } from "../bootstrap" import { EOL } from "os" import { Filesystem } from "../../util/filesystem" -import { getAuthorizationHeader } from "../../util/auth" import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" import { Provider } from "../../provider/provider" @@ -649,8 +648,12 @@ export const RunCommand = cmd({ } if (args.attach) { - const auth = getAuthorizationHeader() - const headers = auth ? { Authorization: auth } : undefined + const headers = (() => { + const password = Flag.OPENCODE_SERVER_PASSWORD + if (!password) return undefined + const auth = `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${password}`).toString("base64")}` + return { Authorization: auth } + })() const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers }) return await execute(sdk) } diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index 7d8b9e4f229..e892f9922d1 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -5,7 +5,6 @@ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { TuiConfig } from "@/config/tui" import { Instance } from "@/project/instance" import { existsSync } from "fs" -import { getAuthorizationHeader } from "@/util/auth" export const AttachCommand = cmd({ command: "attach ", @@ -62,8 +61,9 @@ export const AttachCommand = cmd({ } })() const headers = (() => { - const auth = getAuthorizationHeader(args.password) - if (!auth) return undefined + 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 config = await Instance.provide({ diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 4737aaa6141..e63f10ba80c 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -10,7 +10,6 @@ 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 "@/util/auth" await Log.init({ print: process.argv.includes("--print-logs"), @@ -145,3 +144,9 @@ 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}`)}` +} diff --git a/packages/opencode/src/util/auth.ts b/packages/opencode/src/util/auth.ts deleted file mode 100644 index 1ec44431ce4..00000000000 --- a/packages/opencode/src/util/auth.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Flag } from "../flag/flag" - -export function getAuthorizationHeader( - password = Flag.OPENCODE_SERVER_PASSWORD, - username = Flag.OPENCODE_SERVER_USERNAME, -) { - if (!password) return undefined - return `Basic ${btoa(`${username ?? "opencode"}:${password}`)}` -} diff --git a/packages/opencode/test/cli/run-attach-auth.test.ts b/packages/opencode/test/cli/run-attach-auth.test.ts deleted file mode 100644 index a6577da0d1d..00000000000 --- a/packages/opencode/test/cli/run-attach-auth.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test, expect, describe } from "bun:test" -import { getAuthorizationHeader } from "../../src/util/auth" - -describe("getAuthorizationHeader", () => { - test("returns undefined when no password", () => { - expect(getAuthorizationHeader(undefined)).toBeUndefined() - expect(getAuthorizationHeader("")).toBeUndefined() - }) - - test("returns basic auth with default username", () => { - expect(getAuthorizationHeader("secret")).toBe(`Basic ${btoa("opencode:secret")}`) - }) - - test("uses custom username when provided", () => { - expect(getAuthorizationHeader("secret", "admin")).toBe(`Basic ${btoa("admin:secret")}`) - }) -}) From a2d78d7fd2e016bd9dace9e35198f54d021ca769 Mon Sep 17 00:00:00 2001 From: Eric Clemmons Date: Wed, 4 Mar 2026 22:47:29 -0600 Subject: [PATCH 5/5] fix(opencode): add --password flag and auth headers to run --attach Match attach.ts pattern: accept --password/-p flag and fall back to OPENCODE_SERVER_PASSWORD env var. Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/run.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 76f08b9d8ab..aba8b9134e1 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -280,6 +280,11 @@ export const RunCommand = cmd({ type: "string", describe: "attach to a running opencode server (e.g., http://localhost:4096)", }) + .option("password", { + alias: ["p"], + type: "string", + describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)", + }) .option("dir", { type: "string", describe: "directory to run in, path on remote server if attaching", @@ -649,9 +654,9 @@ export const RunCommand = cmd({ if (args.attach) { const headers = (() => { - const password = Flag.OPENCODE_SERVER_PASSWORD + const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD if (!password) return undefined - const auth = `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${password}`).toString("base64")}` + const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` return { Authorization: auth } })() const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers })