diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx
index 8e6208b140b2..0b6fb720f173 100644
--- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx
@@ -1,16 +1,29 @@
import { TextAttributes, RGBA } from "@opentui/core"
-import { For, type JSX } from "solid-js"
+import { For, Show, createResource, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"
-import { logo, marks } from "@/cli/logo"
+import { Logo as LogoConfig } from "@/cli/logo"
+import { useSDK } from "@tui/context/sdk"
// Shadow markers (rendered chars in parens):
// _ = full shadow cell (space with bg=shadow)
// ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow)
// ~ = shadow top only (▀ with fg=shadow)
-const SHADOW_MARKER = new RegExp(`[${marks}]`)
+const SHADOW_MARKER = new RegExp(`[${LogoConfig.marks}]`)
+
+// Strip ANSI escape codes from text (TUI uses its own color system)
+const ANSI_REGEX = /\x1b\[[0-9;]*m/g
+function stripAnsi(text: string): string {
+ return text.replace(ANSI_REGEX, "")
+}
export function Logo() {
const { theme } = useTheme()
+ const sdk = useSDK()
+
+ const [customLogo] = createResource(async () => {
+ const result = await sdk.client.config.logo({}).catch(() => null)
+ return result?.data
+ })
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
const shadow = tint(theme.background, fg, 0.25)
@@ -71,15 +84,34 @@ export function Logo() {
}
return (
-
-
- {(line, index) => (
-
- {renderLine(line, theme.textMuted, false)}
- {renderLine(logo.right[index()], theme.text, true)}
+
+
+
+ {(line, index) => (
+
+ {renderLine(line, theme.textMuted, false)}
+ {renderLine(LogoConfig.glyphs.right[index()], theme.text, true)}
+
+ )}
+
- )}
-
-
+ }
+ >
+
+
+ {(line) => (
+
+
+ {line}
+
+
+ )}
+
+
+
+
)
}
diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts
index de41f32a0d14..d4139321205e 100644
--- a/packages/opencode/src/cli/cmd/uninstall.ts
+++ b/packages/opencode/src/cli/cmd/uninstall.ts
@@ -53,7 +53,8 @@ export const UninstallCommand = {
handler: async (args: UninstallArgs) => {
UI.empty()
- UI.println(UI.logo(" "))
+ const logoText = await UI.logoAsync(" ")
+ if (logoText) UI.println(logoText)
UI.empty()
prompts.intro("Uninstall OpenCode")
diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts
index 0182056633cb..d01074468714 100644
--- a/packages/opencode/src/cli/cmd/upgrade.ts
+++ b/packages/opencode/src/cli/cmd/upgrade.ts
@@ -21,7 +21,8 @@ export const UpgradeCommand = {
},
handler: async (args: { target?: string; method?: string }) => {
UI.empty()
- UI.println(UI.logo(" "))
+ const logoText = await UI.logoAsync(" ")
+ if (logoText) UI.println(logoText)
UI.empty()
prompts.intro("Upgrade")
const detectedMethod = await Installation.method()
diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts
index 0fe056f21f2f..1662bad26971 100644
--- a/packages/opencode/src/cli/cmd/web.ts
+++ b/packages/opencode/src/cli/cmd/web.ts
@@ -39,7 +39,8 @@ export const WebCommand = cmd({
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
UI.empty()
- UI.println(UI.logo(" "))
+ const logoText = await UI.logoAsync(" ")
+ if (logoText) UI.println(logoText)
UI.empty()
if (opts.hostname === "0.0.0.0") {
diff --git a/packages/opencode/src/cli/logo.ts b/packages/opencode/src/cli/logo.ts
index 44fb93c15b34..38464e9c0f7e 100644
--- a/packages/opencode/src/cli/logo.ts
+++ b/packages/opencode/src/cli/logo.ts
@@ -1,6 +1,53 @@
-export const logo = {
- left: [" ", "█▀▀█ █▀▀█ █▀▀█ █▀▀▄", "█__█ █__█ █^^^ █__█", "▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀"],
- right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"],
+import path from "path"
+import { Config } from "../config/config"
+
+export namespace Logo {
+ // Default OpenCode logo glyphs
+ // Special characters: _ = bg fill, ^ = fg on bg half block, ~ = shadow half block
+ export const glyphs = {
+ left: [" ", "█▀▀█ █▀▀█ █▀▀█ █▀▀▄", "█__█ █__█ █^^^ █__█", "▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀"],
+ right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"],
+ }
+
+ export const marks = "_^~"
+
+ export type Custom = string | false | undefined
+
+ /**
+ * Load a custom logo from the configured path.
+ * Returns the logo content as a string, or undefined to use default, or false to disable.
+ * Gracefully falls back to global config if instance context is not available.
+ */
+ type LogoConfig = { logo?: string | false }
+
+ function parseInlineConfig(): LogoConfig {
+ const inline = process.env.OPENCODE_CONFIG_CONTENT
+ if (!inline) return {}
+ return JSON.parse(inline) as LogoConfig
+ }
+
+ export async function load(): Promise {
+ // Try full config first, fall back to global config, then inline config env var
+ const config: LogoConfig = await Config.get()
+ .catch(() => Config.getGlobal())
+ .catch(() => parseInlineConfig())
+ .catch(() => ({}))
+ if (config.logo === false) return false
+ if (!config.logo) return undefined
+
+ const logoPath = config.logo.startsWith("~")
+ ? path.join(process.env.HOME || "", config.logo.slice(1))
+ : path.isAbsolute(config.logo)
+ ? config.logo
+ : path.resolve(config.logo)
+
+ return Bun.file(logoPath)
+ .text()
+ .then((content) => content.trimEnd())
+ .catch(() => undefined)
+ }
}
-export const marks = "_^~"
+// Legacy export for backwards compatibility
+export const logo = Logo.glyphs
+export const marks = Logo.marks
diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts
index 39396997c65d..7d9b77ea684b 100644
--- a/packages/opencode/src/cli/ui.ts
+++ b/packages/opencode/src/cli/ui.ts
@@ -1,7 +1,7 @@
import z from "zod"
import { EOL } from "os"
import { NamedError } from "@opencode-ai/util/error"
-import { logo as glyphs } from "./logo"
+import { Logo } from "./logo"
export namespace UI {
export const CancelledError = NamedError.create("UICancelledError", z.void())
@@ -40,7 +40,32 @@ export namespace UI {
blank = true
}
+ /**
+ * Render the default OpenCode logo (sync version for CLI help).
+ */
export function logo(pad?: string) {
+ return renderDefaultLogo(pad)
+ }
+
+ /**
+ * Load and render logo based on config (async version).
+ * Uses custom logo if configured, otherwise falls back to default.
+ * Returns empty string if logo is disabled.
+ */
+ export async function logoAsync(pad?: string): Promise {
+ const custom = await Logo.load()
+ if (custom === false) return ""
+ if (custom) return renderCustomLogo(custom, pad)
+ return renderDefaultLogo(pad)
+ }
+
+ function renderCustomLogo(content: string, pad?: string): string {
+ const lines = content.split("\n")
+ if (!pad) return lines.join(EOL)
+ return lines.map((line) => pad + line).join(EOL)
+ }
+
+ function renderDefaultLogo(pad?: string): string {
const result: string[] = []
const reset = "\x1b[0m"
const left = {
@@ -77,11 +102,11 @@ export namespace UI {
}
return parts.join("")
}
- glyphs.left.forEach((row, index) => {
+ Logo.glyphs.left.forEach((row, index) => {
if (pad) result.push(pad)
result.push(draw(row, left.fg, left.shadow, left.bg))
result.push(gap)
- const other = glyphs.right[index] ?? ""
+ const other = Logo.glyphs.right[index] ?? ""
result.push(draw(other, right.fg, right.shadow, right.bg))
result.push(EOL)
})
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index c464fcb64ab8..b1cb8a04b417 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -1039,6 +1039,12 @@ export namespace Config {
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
+ logo: z
+ .union([z.literal(false), z.string()])
+ .optional()
+ .describe(
+ "Custom logo configuration. Set to false to disable, or provide a path to a text file containing the logo (supports ANSI escape codes).",
+ ),
logLevel: Log.Level.optional().describe("Log level"),
server: Server.optional().describe("Server configuration for opencode serve and web commands"),
command: z
diff --git a/packages/opencode/src/server/routes/config.ts b/packages/opencode/src/server/routes/config.ts
index 85d28f6aa6b8..bb1e15d399d9 100644
--- a/packages/opencode/src/server/routes/config.ts
+++ b/packages/opencode/src/server/routes/config.ts
@@ -3,6 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { Config } from "../../config/config"
import { Provider } from "../../provider/provider"
+import { Logo } from "../../cli/logo"
import { mapValues } from "remeda"
import { errors } from "../error"
import { Log } from "../../util/log"
@@ -58,6 +59,36 @@ export const ConfigRoutes = lazy(() =>
return c.json(config)
},
)
+ .get(
+ "/logo",
+ describeRoute({
+ summary: "Get custom logo",
+ description: "Get the custom logo content if configured, or null to use the default logo.",
+ operationId: "config.logo",
+ responses: {
+ 200: {
+ description: "Logo content",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ content: z.string().nullable().describe("Custom logo content, or null to use default"),
+ disabled: z.boolean().describe("Whether the logo is disabled"),
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const custom = await Logo.load()
+ return c.json({
+ content: custom === false ? null : (custom ?? null),
+ disabled: custom === false,
+ })
+ },
+ )
.get(
"/providers",
describeRoute({
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 7a4f4e40cf30..e01d5eb0e5c5 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -16,6 +16,7 @@ import type {
CommandListResponses,
Config as Config3,
ConfigGetResponses,
+ ConfigLogoResponses,
ConfigProvidersResponses,
ConfigUpdateErrors,
ConfigUpdateResponses,
@@ -822,6 +823,25 @@ export class Config2 extends HeyApiClient {
})
}
+ /**
+ * Get custom logo
+ *
+ * Get the custom logo content if configured, or null to use the default logo.
+ */
+ public logo(
+ parameters?: {
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
+ return (options?.client ?? this.client).get({
+ url: "/config/logo",
+ ...options,
+ ...params,
+ })
+ }
+
/**
* List config providers
*
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 86a0c7e42564..3796e183e8b8 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -1312,6 +1312,10 @@ export type Config = {
* JSON schema reference for configuration validation
*/
$schema?: string
+ /**
+ * Custom logo configuration. Set to false to disable, or provide a path to a text file containing the logo (supports ANSI escape codes).
+ */
+ logo?: false | string
logLevel?: LogLevel
server?: ServerConfig
/**
@@ -2451,6 +2455,33 @@ export type ConfigUpdateResponses = {
export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses]
+export type ConfigLogoData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/config/logo"
+}
+
+export type ConfigLogoResponses = {
+ /**
+ * Logo content
+ */
+ 200: {
+ /**
+ * Custom logo content, or null to use default
+ */
+ content: string | null
+ /**
+ * Whether the logo is disabled
+ */
+ disabled: boolean
+ }
+}
+
+export type ConfigLogoResponse = ConfigLogoResponses[keyof ConfigLogoResponses]
+
export type ConfigProvidersData = {
body?: never
path?: never
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index a66ef63647f2..19fc05fb7b5d 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -1052,6 +1052,58 @@
]
}
},
+ "/config/logo": {
+ "get": {
+ "operationId": "config.logo",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get custom logo",
+ "description": "Get the custom logo content if configured, or null to use the default logo.",
+ "responses": {
+ "200": {
+ "description": "Logo content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "Custom logo content, or null to use default",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "disabled": {
+ "description": "Whether the logo is disabled",
+ "type": "boolean"
+ }
+ },
+ "required": ["content", "disabled"]
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.config.logo({\n ...\n})"
+ }
+ ]
+ }
+ },
"/config/providers": {
"get": {
"operationId": "config.providers",
@@ -10419,6 +10471,18 @@
"description": "JSON schema reference for configuration validation",
"type": "string"
},
+ "logo": {
+ "description": "Custom logo configuration. Set to false to disable, or provide a path to a text file containing the logo (supports ANSI escape codes).",
+ "anyOf": [
+ {
+ "type": "boolean",
+ "const": false
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
"logLevel": {
"$ref": "#/components/schemas/LogLevel"
},
diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx
index a9c39bd59f3f..46f6ae7597de 100644
--- a/packages/web/src/content/docs/config.mdx
+++ b/packages/web/src/content/docs/config.mdx
@@ -319,6 +319,41 @@ Set your UI theme in `tui.json`.
---
+### Logo
+
+You can customize or disable the CLI logo through the `logo` option. This is useful for enterprise deployments or custom branding.
+
+```json title="opencode.json"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "logo": "~/.config/opencode/my-logo.txt"
+}
+```
+
+The logo file should contain plain text ASCII art:
+
+```text title="my-logo.txt"
+╔═══════════════════════════╗
+║ ACME Corporation ║
+║ Powered by OpenCode ║
+╚═══════════════════════════╝
+```
+
+To disable the logo entirely:
+
+```json title="opencode.json"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "logo": false
+}
+```
+
+:::note
+The custom logo appears in the TUI home screen and CLI commands like `opencode web`, `opencode upgrade`, and `opencode uninstall`. The CLI help (`--help`) always shows the default OpenCode logo. ANSI escape codes for colors are supported in CLI output but are stripped in the TUI.
+:::
+
+---
+
### Agents
You can configure specialized agents for specific tasks through the `agent` option.