launch: disable Claude Desktop launch (#16028)

This commit is contained in:
Parth Sareen 2026-05-07 10:46:18 -07:00 committed by GitHub
parent bab59072fb
commit f866e7608f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 179 additions and 214 deletions

View file

@ -1201,16 +1201,15 @@ func (db *database) getSettings() (Settings, error) {
func (db *database) setSettings(s Settings) error {
lastHomeView := strings.ToLower(strings.TrimSpace(s.LastHomeView))
validLaunchView := map[string]struct{}{
"launch": {},
"openclaw": {},
"claude": {},
"claude-desktop": {},
"hermes": {},
"codex": {},
"copilot": {},
"opencode": {},
"droid": {},
"pi": {},
"launch": {},
"openclaw": {},
"claude": {},
"hermes": {},
"codex": {},
"copilot": {},
"opencode": {},
"droid": {},
"pi": {},
}
if lastHomeView != "chat" {
if _, ok := validLaunchView[lastHomeView]; !ok {

View file

@ -107,6 +107,21 @@ func TestStore(t *testing.T) {
}
})
t.Run("settings disabled home view falls back to launch", func(t *testing.T) {
if err := s.SetSettings(Settings{LastHomeView: "claude-desktop"}); err != nil {
t.Fatal(err)
}
loaded, err := s.Settings()
if err != nil {
t.Fatal(err)
}
if loaded.LastHomeView != "launch" {
t.Fatalf("expected disabled LastHomeView to fall back to launch, got %q", loaded.LastHomeView)
}
})
t.Run("window size", func(t *testing.T) {
if err := s.SetWindowSize(1024, 768); err != nil {
t.Fatal(err)

View file

@ -13,14 +13,6 @@ interface LaunchCommand {
}
const LAUNCH_COMMANDS: LaunchCommand[] = [
{
id: "claude-desktop",
name: "Claude Desktop",
command: "ollama launch claude-desktop",
description: "Claude Desktop with Ollama Cloud",
icon: "/launch-icons/claude.svg",
iconClassName: "h-7 w-7",
},
{
id: "claude",
name: "Claude Code",

View file

@ -2231,7 +2231,7 @@ func runLauncherAction(cmd *cobra.Command, action tui.TUIAction, deps launcherDe
func launcherActionExitsLoop(integration string) bool {
switch integration {
case "claude-desktop", "vscode":
case "vscode":
return true
default:
return false

View file

@ -249,7 +249,7 @@ func TestRunLauncherAction_GUIAppsExitTUILoop(t *testing.T) {
cmd := &cobra.Command{}
cmd.SetContext(context.Background())
for _, integration := range []string{"claude-desktop", "vscode"} {
for _, integration := range []string{"vscode"} {
continueLoop, err := runLauncherAction(cmd, tui.TUIAction{Kind: tui.TUIActionLaunchIntegration, Integration: integration}, launcherDeps{
resolveRunModel: unexpectedRunModelResolution(t),
launchIntegration: func(ctx context.Context, req launch.IntegrationLaunchRequest) error {

View file

@ -26,6 +26,7 @@ const (
claudeDesktopGatewayBaseURL = "https://ollama.com"
claudeDesktopAPIKeyURL = "https://ollama.com/settings/keys"
claudeDesktopModelLabel = "Ollama Cloud"
claudeDesktopUnsupported = "Claude Desktop is no longer supported. Existing installations can be restored with 'ollama launch claude-desktop --restore'."
claudeDesktopSuccessMessage = "Claude Desktop profile changed to Ollama Cloud."
claudeDesktopRestoreMessage = "To restore the usual Claude profile, run: ollama launch claude-desktop --restore"
claudeDesktopRestoredMessage = "Claude Desktop restored to the usual Claude profile."
@ -129,14 +130,8 @@ func (c *ClaudeDesktop) SkipModelReadiness() bool {
return true
}
func (c *ClaudeDesktop) Run(_ string, args []string) error {
if err := claudeDesktopSupported(); err != nil {
return err
}
if len(args) > 0 {
return fmt.Errorf("claude-desktop does not accept extra arguments")
}
return claudeDesktopLaunchOrRestart("Restart Claude Desktop to use Ollama?")
func (c *ClaudeDesktop) Run(_ string, _ []string) error {
return errClaudeDesktopUnsupported()
}
func (c *ClaudeDesktop) Restore() error {
@ -167,6 +162,10 @@ func (c *ClaudeDesktop) Restore() error {
return claudeDesktopLaunchOrRestart("Restart Claude Desktop to use the usual Claude profile?")
}
func errClaudeDesktopUnsupported() error {
return errors.New(claudeDesktopUnsupported)
}
func claudeDesktopSupported() error {
switch claudeDesktopGOOS {
case "darwin", "windows":
@ -838,7 +837,7 @@ func defaultClaudeDesktopOpenApp() error {
if path := claudeDesktopRunningAppPath(); path != "" {
return claudeDesktopOpenAppPath(path)
}
return fmt.Errorf("Claude Desktop executable was not found; open Claude Desktop manually once and re-run 'ollama launch claude-desktop'")
return fmt.Errorf("Claude Desktop executable was not found; open Claude Desktop manually once and re-run 'ollama launch claude-desktop --restore'")
case "darwin":
return openClaudeDesktopDarwin()
default:

View file

@ -4,10 +4,8 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
@ -129,62 +127,59 @@ func TestClaudeDesktopIntegration(t *testing.T) {
})
}
func TestLaunchIntegration_ClaudeDesktopDoesNotRequireLocalCloudSignIn(t *testing.T) {
func TestLaunchIntegration_ClaudeDesktopLaunchReturnsUnsupported(t *testing.T) {
for _, name := range []string{"claude-desktop", "claude-app"} {
t.Run(name, func(t *testing.T) {
err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: name})
if err == nil {
t.Fatal("expected Claude Desktop launch to fail")
}
if !strings.Contains(err.Error(), "Claude Desktop is no longer supported") {
t.Fatalf("expected unsupported guidance, got %v", err)
}
if !strings.Contains(err.Error(), "ollama launch claude-desktop --restore") {
t.Fatalf("expected restore guidance, got %v", err)
}
})
}
}
func TestLaunchIntegration_ClaudeDesktopRestoreStillWorks(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
withClaudeDesktopPlatform(t, "darwin")
withInteractiveSession(t, true)
withLauncherHooks(t)
t.Setenv("OLLAMA_API_KEY", "test-api-key")
withClaudeDesktopProcessHooks(t, func() bool { return false }, func() error { return nil }, func() error { return nil })
if err := os.MkdirAll(filepath.Join(tmpDir, "Applications", "Claude.app"), 0o755); err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/status":
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `{"error":"not found"}`)
case "/api/me":
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, `{"error":"unauthorized","signin_url":"https://example.com/signin"}`)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
t.Setenv("OLLAMA_HOST", srv.URL)
paths, err := claudeDesktopConfigPaths()
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(paths.profile), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(paths.meta, []byte(`{"appliedId":"`+claudeDesktopProfileID+`","entries":[{"id":"`+claudeDesktopProfileID+`","name":"Ollama"}]}`), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(paths.profile, []byte(`{"disableDeploymentModeChooser":true,"inferenceGatewayApiKey":"keep","inferenceProvider":"gateway","inferenceGatewayBaseUrl":"https://ollama.com","inferenceGatewayAuthScheme":"bearer"}`), 0o644); err != nil {
t.Fatal(err)
}
var validatedKey string
withClaudeDesktopValidation(t, func(_ context.Context, key string) error {
validatedKey = key
return nil
stderr := captureStderr(t, func() {
err = LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "claude-desktop", Restore: true})
})
DefaultSignIn = func(modelName, signInURL string) (string, error) {
t.Fatalf("Claude Desktop launch should not require local Ollama Cloud sign-in, got %s at %s", modelName, signInURL)
return "", nil
if err != nil {
t.Fatalf("LaunchIntegration restore returned error: %v", err)
}
var openCalls int
withClaudeDesktopProcessHooks(t,
func() bool { return false },
func() error { return nil },
func() error {
openCalls++
return nil
},
)
if err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "claude-desktop"}); err != nil {
t.Fatalf("LaunchIntegration returned error: %v", err)
if !strings.Contains(stderr, claudeDesktopRestoredMessage) {
t.Fatalf("expected restore success message, got stderr: %q", stderr)
}
if validatedKey != "test-api-key" {
t.Fatalf("validated key = %q, want test API key", validatedKey)
}
if openCalls != 1 {
t.Fatalf("open calls = %d, want 1", openCalls)
desktopConfig := claudeDesktopReadJSON(t, paths.desktopConfig)
if desktopConfig["deploymentMode"] != "1p" {
t.Fatalf("deploymentMode = %v, want 1p", desktopConfig["deploymentMode"])
}
}
@ -472,11 +467,28 @@ func TestWaitForClaudeDesktopExitUsesRunningHook(t *testing.T) {
}
}
func TestClaudeDesktopWindowsRestartUsesCapturedDesktopPath(t *testing.T) {
func TestClaudeDesktopWindowsRestoreRestartUsesCapturedDesktopPath(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
withClaudeDesktopPlatform(t, "windows")
t.Setenv("LOCALAPPDATA", filepath.Join(tmpDir, "LocalAppData"))
restoreConfirm := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})
defer restoreConfirm()
paths, err := claudeDesktopConfigPaths()
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(paths.profile), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(paths.meta, []byte(`{"appliedId":"`+claudeDesktopProfileID+`","entries":[{"id":"`+claudeDesktopProfileID+`","name":"Ollama"}]}`), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(paths.profile, []byte(`{"disableDeploymentModeChooser":true,"inferenceGatewayApiKey":"keep"}`), 0o644); err != nil {
t.Fatal(err)
}
desktopPath := `C:\Users\parth\AppData\Local\AnthropicClaude\app-1.2.3\Claude.exe`
running := true
var openedPath string
@ -497,8 +509,8 @@ func TestClaudeDesktopWindowsRestartUsesCapturedDesktopPath(t *testing.T) {
return nil
}
if err := (&ClaudeDesktop{}).Run("qwen3.5", nil); err != nil {
t.Fatalf("Run returned error: %v", err)
if err := (&ClaudeDesktop{}).Restore(); err != nil {
t.Fatalf("Restore returned error: %v", err)
}
if openedPath != desktopPath {
t.Fatalf("opened path = %q, want %q", openedPath, desktopPath)
@ -901,38 +913,34 @@ func TestClaudeDesktopRestoreTouchesAllWindowsProfileCandidates(t *testing.T) {
}
}
func TestClaudeDesktopRunRestartsRunningAppWhenConfirmed(t *testing.T) {
func TestClaudeDesktopRunReturnsUnsupported(t *testing.T) {
withClaudeDesktopPlatform(t, "darwin")
restoreConfirm := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})
defer restoreConfirm()
running := true
var quitCalls, openCalls int
withClaudeDesktopProcessHooks(t,
func() bool { return running },
func() bool {
t.Fatal("Run should not inspect Claude Desktop process state")
return false
},
func() error {
quitCalls++
running = false
t.Fatal("Run should not quit Claude Desktop")
return nil
},
func() error {
openCalls++
t.Fatal("Run should not open Claude Desktop")
return nil
},
)
if err := (&ClaudeDesktop{}).Run("qwen3.5", nil); err != nil {
t.Fatalf("Run returned error: %v", err)
}
if quitCalls != 1 || openCalls != 1 {
t.Fatalf("quit/open calls = %d/%d, want 1/1", quitCalls, openCalls)
}
}
func TestClaudeDesktopRunRejectsExtraArgs(t *testing.T) {
withClaudeDesktopPlatform(t, "darwin")
err := (&ClaudeDesktop{}).Run("qwen3.5", []string{"--foo"})
if err == nil || !strings.Contains(err.Error(), "does not accept extra arguments") {
t.Fatalf("Run error = %v, want extra args rejection", err)
for _, args := range [][]string{nil, {"--foo"}} {
err := (&ClaudeDesktop{}).Run("qwen3.5", args)
if err == nil {
t.Fatal("expected Run to fail")
}
if !strings.Contains(err.Error(), "Claude Desktop is no longer supported") {
t.Fatalf("expected unsupported guidance, got %v", err)
}
if !strings.Contains(err.Error(), "ollama launch claude-desktop --restore") {
t.Fatalf("expected restore guidance, got %v", err)
}
}
}

View file

@ -233,6 +233,31 @@ func TestLaunchCmdTUICallback(t *testing.T) {
})
}
func TestLaunchCmdClaudeDesktopLaunchReturnsUnsupported(t *testing.T) {
for _, name := range []string{"claude-desktop", "claude-app"} {
t.Run(name, func(t *testing.T) {
cmd := LaunchCmd(func(cmd *cobra.Command, args []string) error {
t.Fatal("heartbeat check should not run before Claude Desktop unsupported error")
return nil
}, func(cmd *cobra.Command) {
t.Fatal("TUI callback should not run for direct integration launch")
})
cmd.SetArgs([]string{name})
err := cmd.Execute()
if err == nil {
t.Fatal("expected Claude Desktop launch command to fail")
}
if !strings.Contains(err.Error(), "Claude Desktop is no longer supported") {
t.Fatalf("expected unsupported guidance, got %v", err)
}
if !strings.Contains(err.Error(), "ollama launch claude-desktop --restore") {
t.Fatalf("expected restore guidance, got %v", err)
}
})
}
}
func TestLaunchCmdNilHeartbeat(t *testing.T) {
cmd := LaunchCmd(nil, nil)
if cmd == nil {

View file

@ -140,6 +140,23 @@ func TestLookupIntegration_UnknownIntegration(t *testing.T) {
}
}
func TestLookupIntegration_ClaudeDesktopResolvesForRestore(t *testing.T) {
for _, name := range []string{"claude-desktop", "claude-app"} {
t.Run(name, func(t *testing.T) {
canonical, runner, err := LookupIntegration(name)
if err != nil {
t.Fatalf("expected Claude Desktop lookup to resolve, got: %v", err)
}
if canonical != "claude-desktop" {
t.Fatalf("canonical name = %q, want claude-desktop", canonical)
}
if runner.String() != "Claude Desktop" {
t.Fatalf("runner = %q, want Claude Desktop", runner.String())
}
})
}
}
func TestIsIntegrationInstalled_UnknownIntegrationReturnsFalse(t *testing.T) {
stderr := captureStderr(t, func() {
if IsIntegrationInstalled("unknown-integration") {
@ -1715,11 +1732,6 @@ func TestIntegration_InstallHint(t *testing.T) {
input: "claude",
wantURL: "https://code.claude.com/docs/en/quickstart",
},
{
name: "claude desktop has hint",
input: "claude-desktop",
wantURL: "https://claude.com/download",
},
{
name: "codex has hint",
input: "codex",
@ -1801,16 +1813,6 @@ func TestListIntegrationInfos(t *testing.T) {
}
want = filtered
}
if claudeDesktopSupported() != nil {
filtered := make([]string, 0, len(want))
for _, name := range want {
if name != "claude-desktop" {
filtered = append(filtered, name)
}
}
want = filtered
}
if diff := compareStrings(got, want); diff != "" {
t.Fatalf("launcher integration order mismatch: %s", diff)
}
@ -1829,9 +1831,6 @@ func TestListIntegrationInfos(t *testing.T) {
t.Run("includes known integrations", func(t *testing.T) {
known := map[string]bool{"claude": false, "codex": false, "opencode": false}
if claudeDesktopSupported() == nil {
known["claude-desktop"] = false
}
if poolsideGOOS != "windows" {
known["pool"] = false
}
@ -1882,14 +1881,10 @@ func TestListIntegrationInfos_HidesPoolsideOnWindows(t *testing.T) {
}
}
func TestListIntegrationInfos_HidesClaudeDesktopOnUnsupportedPlatform(t *testing.T) {
prev := claudeDesktopGOOS
claudeDesktopGOOS = "linux"
t.Cleanup(func() { claudeDesktopGOOS = prev })
func TestListIntegrationInfos_HidesClaudeDesktop(t *testing.T) {
for _, info := range ListIntegrationInfos() {
if info.Name == "claude-desktop" {
t.Fatal("expected claude-desktop to be hidden on unsupported platforms")
t.Fatal("expected hidden claude-desktop to be absent")
}
}
}

View file

@ -285,7 +285,6 @@ Flags and extra arguments require an integration name.
Supported integrations:
claude Claude Code
claude-desktop Claude Desktop (aliases: claude-app)
cline Cline
codex Codex
copilot Copilot CLI (aliases: copilot-cli)
@ -302,8 +301,6 @@ Examples:
ollama launch
ollama launch claude
ollama launch claude --model <model>
ollama launch claude-desktop
ollama launch claude-desktop --restore
ollama launch hermes
ollama launch droid --config (does not auto-launch)
ollama launch codex -- -p myprofile (pass extra args to integration)
@ -350,6 +347,10 @@ Examples:
return nil
}
if !restoreFlag && launchCommandIsClaudeDesktop(name) {
return errClaudeDesktopUnsupported()
}
if modelFlag != "" && isCloudModelName(modelFlag) {
if client, err := api.ClientFromEnvironment(); err == nil {
if disabled, _ := cloudStatusDisabled(cmd.Context(), client); disabled {
@ -395,8 +396,12 @@ func launchCommandCanSkipHeartbeat(args []string) bool {
if len(args) == 0 {
return false
}
name, _, err := LookupIntegration(args[0])
return err == nil && name == "claude-desktop"
return launchCommandIsClaudeDesktop(args[0])
}
func launchCommandIsClaudeDesktop(name string) bool {
canonical, _, err := LookupIntegration(name)
return err == nil && canonical == claudeDesktopIntegrationName
}
type launcherClient struct {
@ -459,6 +464,10 @@ func LaunchIntegration(ctx context.Context, req IntegrationLaunchRequest) error
return err
}
if name == claudeDesktopIntegrationName && !req.Restore {
return errClaudeDesktopUnsupported()
}
policy := launchIntegrationPolicy(req)
if req.Restore {
return restoreIntegration(name, runner, req)

View file

@ -33,7 +33,7 @@ type IntegrationInfo struct {
Description string
}
var launcherIntegrationOrder = []string{"claude-desktop", "claude", "openclaw", "hermes", "opencode", "codex", "copilot", "droid", "pi", "pool"}
var launcherIntegrationOrder = []string{"claude", "openclaw", "hermes", "opencode", "codex", "copilot", "droid", "pi", "pool"}
var integrationSpecs = []*IntegrationSpec{
{
@ -53,6 +53,7 @@ var integrationSpecs = []*IntegrationSpec{
Runner: &ClaudeDesktop{},
Aliases: []string{"claude-app"},
Description: "Claude Desktop with Ollama Cloud",
Hidden: true,
Install: IntegrationInstallSpec{
CheckInstalled: func() bool {
return claudeDesktopInstalled()

View file

@ -44,13 +44,6 @@ func launcherTestState() *launch.LauncherState {
Selectable: true,
Changeable: true,
},
"claude-desktop": {
Name: "claude-desktop",
DisplayName: "Claude Desktop",
Description: "Claude Desktop with Ollama Cloud",
Selectable: true,
Changeable: true,
},
"hermes": {
Name: "hermes",
DisplayName: "Hermes Agent",
@ -135,12 +128,8 @@ func TestMenuRendersPinnedItemsAndMore(t *testing.T) {
t.Fatalf("expected menu view to contain %q\n%s", want, view)
}
}
if findMenuCursorByIntegration(menu.items, "claude-desktop") == -1 {
if strings.Contains(view, "Launch Claude Desktop") {
t.Fatalf("expected Claude Desktop to be hidden on unsupported platforms\n%s", view)
}
} else if !strings.Contains(view, "Launch Claude Desktop") {
t.Fatalf("expected menu view to contain Claude Desktop\n%s", view)
if strings.Contains(view, "Launch Claude Desktop") {
t.Fatalf("expected hidden Claude Desktop to be absent\n%s", view)
}
wantOrder := expectedCollapsedSequence(state)
if diff := compareStrings(integrationSequence(menu.items), wantOrder); diff != "" {

View file

@ -115,8 +115,7 @@
"expanded": true,
"pages": [
"/integrations/openclaw",
"/integrations/hermes",
"/integrations/claude-desktop"
"/integrations/hermes"
]
},
{

View file

@ -2,77 +2,12 @@
title: Claude Desktop
---
Claude Desktop can use Ollama Cloud, including Claude Cowork and Claude Code inside the app.
Claude Desktop is no longer supported by `ollama launch`.
<img
src="/images/claude-cowork-kimi-k2-6.png"
alt="Claude Cowork using kimi-k2.6 through Ollama Cloud"
className="rounded-xl"
/>
## Requirements
- Claude Desktop for macOS or Windows
- An [Ollama API key](https://ollama.com/settings/keys)
Set the key in your shell before launching:
```shell
export OLLAMA_API_KEY=your_api_key
```
## Quick setup
```shell
ollama launch claude-desktop
```
To bring back the usual Anthropic Claude profile later, run:
Existing installations can be restored to the usual Claude profile:
```shell
ollama launch claude-desktop --restore
```
## Using Ollama Cloud models
After setup, Claude Desktop discovers your available Ollama Cloud models automatically. For example, `kimi-k2.6` appears inside Claude Cowork.
The same models are also available to Claude Code inside Claude Desktop:
![Claude Code using kimi-k2.6 inside Claude Desktop](/images/claude-code-kimi-k2-6.png)
## Configure without launching
```shell
ollama launch claude-desktop --config
```
## Restore normal Claude
Switch Claude Desktop back to its usual profile:
```shell
ollama launch claude-desktop --restore
```
If Claude Desktop is running, use `--yes` to approve the restart automatically:
```shell
ollama launch claude-desktop --restore --yes
```
## Supported with Ollama
Claude Desktop with Ollama currently supports:
- Ollama Cloud as the third-party inference gateway
- Automatic model discovery from Ollama Cloud
- Claude Cowork with Ollama Cloud models
- Claude Code inside Claude Desktop with the same cloud models
- subagents (tell Claude to have subagents inherit the current model)
Claude Desktop with Ollama does not support yet:
- Web search
- Extensions
Use [Claude Code](/integrations/claude-code) for Anthropic-compatible coding workflows with Ollama.

View file

@ -23,7 +23,6 @@ AI assistants that help with everyday tasks.
- [OpenClaw](/integrations/openclaw)
- [Hermes Agent](/integrations/hermes)
- [Claude Desktop](/integrations/claude-desktop)
## IDEs & Editors