Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1748,6 +1748,10 @@ export const de: Record<TKey, string> = {
"claudeDesktop.retry": "Erneut versuchen",
"claudeDesktop.saveFailed": "Claude-Desktop-Profil konnte nicht gespeichert werden.",
"claudeDesktop.applyFailed": "Das Profil wurde gespeichert, konnte aber nicht angewendet werden.",
"claudeDesktop.toggleFailed": "Fehler beim Umschalten des Claude-Desktop-Routings.",
"claudeDesktop.enabledLabel": "Claude-Desktop-Routing",
"claudeDesktop.toggleAria": "Claude-Desktop-Routing ein- oder ausschalten",
"claudeDesktop.disabledNotice": "Claude-Desktop-Routing ist ausgeschaltet. Schalten Sie es ein, um Änderungen auf Claude Desktop anzuwenden.",
"claudeDesktop.updateFailed": "Claude-Desktop-Aktualisierung fehlgeschlagen.",
"claudeDesktop.savedApplied": "Profil gespeichert und auf Claude Desktop angewendet.",
"claudeDesktop.savedAppliedAnnounce": "Claude-Desktop-Profil gespeichert und angewendet.",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,10 @@ export const en = {
"claudeDesktop.retry": "Retry",
"claudeDesktop.saveFailed": "Failed to save Claude Desktop profile.",
"claudeDesktop.applyFailed": "Profile was saved, but could not be applied.",
"claudeDesktop.toggleFailed": "Failed to toggle Claude Desktop routing.",
"claudeDesktop.enabledLabel": "Claude Desktop routing",
"claudeDesktop.toggleAria": "Toggle Claude Desktop routing on or off",
"claudeDesktop.disabledNotice": "Claude Desktop routing is off. Toggle it on to apply changes to Claude Desktop.",
"claudeDesktop.updateFailed": "Claude Desktop update failed.",
"claudeDesktop.savedApplied": "Profile saved and applied to Claude Desktop.",
"claudeDesktop.savedAppliedAnnounce": "Claude Desktop profile saved and applied.",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,10 @@ export const ja: Record<TKey, string> = {
"claudeDesktop.retry": "再試行",
"claudeDesktop.saveFailed": "Claude Desktop プロファイルの保存に失敗しました。",
"claudeDesktop.applyFailed": "プロファイルは保存されましたが、適用できませんでした。",
"claudeDesktop.toggleFailed": "Claude Desktop ルーティングの切り替えに失敗しました。",
"claudeDesktop.enabledLabel": "Claude Desktop ルーティング",
"claudeDesktop.toggleAria": "Claude Desktop ルーティングをオンまたはオフに切り替え",
"claudeDesktop.disabledNotice": "Claude Desktop ルーティングはオフです。オンに切り替えると、変更が Claude Desktop に適用されます。",
"claudeDesktop.updateFailed": "Claude Desktop の更新に失敗しました。",
"claudeDesktop.savedApplied": "プロファイルを保存し、Claude Desktop に適用しました。",
"claudeDesktop.savedAppliedAnnounce": "Claude Desktop プロファイルを保存して適用しました。",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,10 @@ export const ko: Record<TKey, string> = {
"claudeDesktop.retry": "다시 시도",
"claudeDesktop.saveFailed": "Claude Desktop 프로필을 저장하지 못했습니다.",
"claudeDesktop.applyFailed": "프로필은 저장했지만 적용하지 못했습니다.",
"claudeDesktop.toggleFailed": "Claude Desktop 라우팅 전환에 실패했습니다.",
"claudeDesktop.enabledLabel": "Claude Desktop 라우팅",
"claudeDesktop.toggleAria": "Claude Desktop 라우팅 켜기/끄기",
"claudeDesktop.disabledNotice": "Claude Desktop 라우팅이 꺼져 있습니다. 켜면 변경 사항이 Claude Desktop에 적용됩니다.",
"claudeDesktop.updateFailed": "Claude Desktop 업데이트에 실패했습니다.",
"claudeDesktop.savedApplied": "프로필을 저장하고 Claude Desktop에 적용했습니다.",
"claudeDesktop.savedAppliedAnnounce": "Claude Desktop 프로필 저장과 적용을 마쳤습니다.",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,10 @@ export const ru: Record<TKey, string> = {
"claudeDesktop.retry": "Повторить",
"claudeDesktop.saveFailed": "Не удалось сохранить профиль Claude Desktop.",
"claudeDesktop.applyFailed": "Профиль сохранён, но применить его не удалось.",
"claudeDesktop.toggleFailed": "Не удалось переключить маршрутизацию Claude Desktop.",
"claudeDesktop.enabledLabel": "Маршрутизация Claude Desktop",
"claudeDesktop.toggleAria": "Включить или выключить маршрутизацию Claude Desktop",
"claudeDesktop.disabledNotice": "Маршрутизация Claude Desktop отключена. Включите её, чтобы применить изменения.",
"claudeDesktop.updateFailed": "Не удалось обновить Claude Desktop.",
"claudeDesktop.savedApplied": "Профиль сохранён и применён к Claude Desktop.",
"claudeDesktop.savedAppliedAnnounce": "Профиль Claude Desktop сохранён и применён.",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1768,6 +1768,10 @@ export const zh: Record<TKey, string> = {
"claudeDesktop.retry": "重试",
"claudeDesktop.saveFailed": "无法保存 Claude Desktop 配置。",
"claudeDesktop.applyFailed": "配置已保存,但无法应用。",
"claudeDesktop.toggleFailed": "切换 Claude Desktop 路由失败。",
"claudeDesktop.enabledLabel": "Claude Desktop 路由",
"claudeDesktop.toggleAria": "开启或关闭 Claude Desktop 路由",
"claudeDesktop.disabledNotice": "Claude Desktop 路由已关闭。开启以将更改应用到 Claude Desktop。",
"claudeDesktop.updateFailed": "Claude Desktop 更新失败。",
"claudeDesktop.savedApplied": "配置已保存并应用到 Claude Desktop。",
"claudeDesktop.savedAppliedAnnounce": "Claude Desktop 配置已保存并应用。",
Expand Down
63 changes: 58 additions & 5 deletions gui/src/pages/ClaudeDesktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, ty
import { LANE_PAGE, defaultCollapsedFamilies, laneView, rowStartsOpen } from "./claude-desktop-lane";
import { makeCollapseStore, toggleInSet } from "./collapse-store";
import { IconChevron } from "../icons";
import { EmptyState, Notice } from "../ui";
import { EmptyState, Notice, Switch } from "../ui";
import { LOCALES, useI18n, type TFn, type TKey } from "../i18n/shared";
import { readJsonIfOk, readJsonOrThrow } from "../fetch-json";
import { readSessionListCache, writeSessionListCache } from "../session-list-cache";
Expand Down Expand Up @@ -60,6 +60,7 @@ interface DesktopResponse {
models: DesktopModel[];
rendered: unknown[];
port: number;
enabled: boolean;
}

type PendingAction = "save" | "apply" | null;
Expand Down Expand Up @@ -164,6 +165,9 @@ export default function ClaudeDesktop({
const [message, setMessage] = useState<{ tone: "ok" | "err"; text: string } | null>(null);
const [announcement, setAnnouncement] = useState("");
const [pending, setPending] = useState<PendingAction>(null);
const [desktopEnabled, setDesktopEnabled] = useState<boolean>(() => cached?.data?.enabled !== false);
const [connectionPending, setConnectionPending] = useState(false);
const connectionInFlight = useRef(false);
// Lane density: search and paging are RENDER-ONLY. modelsByFamily and effectiveDefaults must
// keep seeing every model — filtering the source arrays would silently change which model is
// the effective default, turning a view filter into a data mutation.
Expand Down Expand Up @@ -195,6 +199,7 @@ export default function ClaudeDesktop({
setProfile(normalized);
setSavedProfile(cloneProfile(normalized));
setDestinations(Object.fromEntries(payload.models.map(model => [model.route, normalized.assignments[model.route]?.family ?? "opus"])));
setDesktopEnabled(payload.enabled !== false);
// Fold empty families on load, but only while the user has no stored preference.
// Doing it here rather than per render means a later move or import can never
// re-fold a section the user opened.
Expand Down Expand Up @@ -304,6 +309,39 @@ export default function ClaudeDesktop({
setCollapsedFamilies(next);
};

/**
* Immediate on/off toggle for the Desktop 3P config files.
* Waits for the response — writing to another program's config library is
* not something we should optimistically claim succeeded.
*/
const toggleDesktop = async () => {
if (connectionInFlight.current) return;
connectionInFlight.current = true;
setConnectionPending(true);
setMessage(null);
const next = !desktopEnabled;
try {
const response = await fetch(`${apiBase}/api/native-integrations/claude-desktop`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: next }),
});
const body = await readJsonOrThrow<{ ok?: boolean; message?: string; state?: string }>(response, t("claudeDesktop.toggleFailed"));
Comment on lines +317 to +329

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse toggleNativeIntegration instead of a raw fetch for the Desktop toggle.

gui/src/pages/integrations/native-api.ts already exports toggleNativeIntegration(apiBase, client, enabled, signal), which PUTs the same endpoint shape (/api/native-integrations/${client}), returns a typed NativeToggleEnvelope (ok, clientId, changed, state, message, reason), and throws a typed NativeApiError with a parsed .refusal (including disableBlocked reason codes like not_installed or config_busy) on failure. NativeIntegrationClientId now includes "claude-desktop" (native-api.ts Line 11), so this helper is directly usable here.

The raw fetch + readJsonOrThrow path in toggleDesktop (Lines 317-329) bypasses that refusal-aware parsing. A native refusal from the server (for example, the config file busy or Desktop not installed) will still throw via readJsonOrThrow's error path, but only as a flat string pulled from error/message, losing the reason code that the rest of the native-integration surface (claudeRow, grokRow, toggleBlocked: native?.disableBlocked) uses to drive localized, reason-specific messaging.

Do you want me to generate a version of toggleDesktop that calls toggleNativeIntegration and maps NativeApiError.refusal?.message into the existing setMessage/setAnnouncement flow?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/pages/ClaudeDesktop.tsx` around lines 317 - 329, Update toggleDesktop
to call toggleNativeIntegration(apiBase, "claude-desktop", next, signal) instead
of using raw fetch and readJsonOrThrow. Preserve the existing pending-state and
success handling, and catch NativeApiError so its refusal message and
reason-specific data are mapped into the existing setMessage/setAnnouncement
flow.

setDesktopEnabled(next);
if (body.message) {
setMessage({ tone: "ok", text: body.message });
setAnnouncement(body.message);
}
void desktopResource.refresh();
void statusResource.refresh();
Comment on lines +317 to +336

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unguarded body.message access on a possibly-undefined value.

readJsonOrThrow is declared as Promise<T | undefined> (see its definition in gui/src/fetch-json.ts, also surfaced in this PR's own graph context). At Line 329, body therefore has type { ok?: boolean; message?: string; state?: string } | undefined. Line 331 then reads body.message with no null check.

Every other call site of readJsonOrThrow in this same file (Lines 189, 355, 361) either discards the return value or only relies on the throw-on-!res.ok side effect. This is the only place the return value is stored and its property accessed directly, and the only place missing a guard.

Failure mode: if the PUT response is res.ok but the body doesn't parse into JSON (empty body, wrong Content-Type, truncated response), readJsonOrThrow returns undefined, and body.message throws TypeError: Cannot read properties of undefined (reading 'message') inside the try block's own success path — this crash is not caught by the surrounding catch's intended "toggle failed" messaging path in a meaningful way (it is caught, but reports a generic JS error instead of the real failure), and it also means setDesktopEnabled(next) on Line 330 has already run, leaving the toggle state locally flipped even though the "success" handling then throws.

🐛 Proposed fix
-      const body = await readJsonOrThrow<{ ok?: boolean; message?: string; state?: string }>(response, t("claudeDesktop.toggleFailed"));
+      const body = await readJsonOrThrow<{ ok?: boolean; message?: string; state?: string }>(response, t("claudeDesktop.toggleFailed")) ?? {};
       setDesktopEnabled(next);
       if (body.message) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const toggleDesktop = async () => {
if (connectionInFlight.current) return;
connectionInFlight.current = true;
setConnectionPending(true);
setMessage(null);
const next = !desktopEnabled;
try {
const response = await fetch(`${apiBase}/api/native-integrations/claude-desktop`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: next }),
});
const body = await readJsonOrThrow<{ ok?: boolean; message?: string; state?: string }>(response, t("claudeDesktop.toggleFailed"));
setDesktopEnabled(next);
if (body.message) {
setMessage({ tone: "ok", text: body.message });
setAnnouncement(body.message);
}
void desktopResource.refresh();
void statusResource.refresh();
const toggleDesktop = async () => {
if (connectionInFlight.current) return;
connectionInFlight.current = true;
setConnectionPending(true);
setMessage(null);
const next = !desktopEnabled;
try {
const response = await fetch(`${apiBase}/api/native-integrations/claude-desktop`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: next }),
});
const body = await readJsonOrThrow<{ ok?: boolean; message?: string; state?: string }>(response, t("claudeDesktop.toggleFailed")) ?? {};
setDesktopEnabled(next);
if (body.message) {
setMessage({ tone: "ok", text: body.message });
setAnnouncement(body.message);
}
void desktopResource.refresh();
void statusResource.refresh();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/pages/ClaudeDesktop.tsx` around lines 317 - 336, Guard the optional
result from readJsonOrThrow in toggleDesktop before accessing body.message,
while preserving the successful setDesktopEnabled flow for valid responses. Only
display the server message and announcement when body exists and contains a
message, so an empty or invalid successful response cannot throw after the local
state is updated.

} catch (error) {
setMessage({ tone: "err", text: error instanceof Error ? error.message : t("claudeDesktop.toggleFailed") });
} finally {
connectionInFlight.current = false;
setConnectionPending(false);
}
};

const save = async (applyAfter: boolean) => {
if (!profile || pending) return;
setPending("save");
Expand Down Expand Up @@ -392,6 +430,15 @@ export default function ClaudeDesktop({
return (
<>
{/* Title/subtitle live on Claude.tsx above the Code/Desktop strip. */}
<div className="claudecode-connection-head">
<span id="claude-desktop-connection-label">{t("claudeDesktop.enabledLabel")}</span>
<Switch
on={desktopEnabled}
onClick={() => void toggleDesktop()}
disabled={connectionPending}
label={t("claudeDesktop.toggleAria")}
/>
</div>
<div className="claude-desktop-toolbar">
<div className="claude-profile-tools">
<input ref={importRef} type="file" accept="application/json,.json" hidden onChange={event => void importProfile(event)} />
Expand All @@ -400,6 +447,10 @@ export default function ClaudeDesktop({
</div>
</div>

{!desktopEnabled && (
<Notice tone="err">{t("claudeDesktop.disabledNotice")}</Notice>
)}

{/* Always mount the bar (pending strut when status is still cold) so a late /status
response cannot insert a full row under the title and shove the lanes down. */}
<div
Expand Down Expand Up @@ -455,12 +506,14 @@ export default function ClaudeDesktop({
<div className="claude-profile-bar">
<span className={`claude-dirty${dirty ? " active" : ""}`}>{dirty ? t("claudeDesktop.unsaved") : t("claudeDesktop.upToDate")}</span>
<div className="claude-save-actions">
<button type="button" className="btn btn-ghost" disabled={!dirty || pending !== null} onClick={() => void save(false)}>
<button type="button" className={`btn ${desktopEnabled ? "btn-ghost" : "btn-primary"}`} disabled={!dirty || pending !== null} onClick={() => void save(false)}>
{pending === "save" ? t("claudeDesktop.saving") : t("common.save")}
</button>
<button type="button" className="btn btn-primary" disabled={pending !== null} onClick={() => void save(true)}>
{pending === "apply" ? t("claudeDesktop.applying") : pending === "save" ? t("claudeDesktop.saving") : t("claudeDesktop.saveApply")}
</button>
{desktopEnabled && (
<button type="button" className="btn btn-primary" disabled={pending !== null} onClick={() => void save(true)}>
{pending === "apply" ? t("claudeDesktop.applying") : pending === "save" ? t("claudeDesktop.saving") : t("claudeDesktop.saveApply")}
</button>
)}
</div>
</div>

Expand Down
2 changes: 2 additions & 0 deletions gui/src/pages/integrations/integration-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,15 @@ export async function loadClaudeCodeStatus(apiBase: string, signal?: AbortSignal

export async function loadClaudeDesktopStatus(apiBase: string, signal?: AbortSignal) {
const body = await readOptional<{
enabled?: unknown;
applied?: unknown;
stale?: unknown;
activeProfile?: unknown;
appliedAt?: unknown;
}>(fetch(`${apiBase}/api/claude-desktop/status`, { signal }));
if (!body) return null;
return {
enabled: body.enabled !== false,
applied: body.applied === true,
stale: body.stale === true,
// Tri-state on purpose: `null` means undeterminable, which must not be
Expand Down
4 changes: 2 additions & 2 deletions gui/src/pages/integrations/native-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { readJsonIfOk } from "../../fetch-json";
* nothing caught it locally because GUI typecheck runs from its own tsconfig —
* `bun x tsc --noEmit` at the repository root does not read this file. CI did.
*/
export type NativeIntegrationClientId = "claude" | "grok" | "codex";
export type NativeIntegrationClientId = "claude" | "grok" | "codex" | "claude-desktop";
export type NativeIntegrationState = "absent" | "current" | "unsafe";
export type NativeRefusalReason =
| "not_installed"
Expand Down Expand Up @@ -58,7 +58,7 @@ export type NativeErrorBody = NativeErrorEnvelope | NativeRefusalEnvelope;

// Widening the type alone would leave this guard rejecting a `codex` response at
// runtime, so the set moves with it.
const NATIVE_CLIENTS: ReadonlySet<string> = new Set<NativeIntegrationClientId>(["claude", "grok", "codex"]);
const NATIVE_CLIENTS: ReadonlySet<string> = new Set<NativeIntegrationClientId>(["claude", "grok", "codex", "claude-desktop"]);
const NATIVE_REFUSAL_CODES: ReadonlySet<string> = new Set([
"native_integration_refused",
"native_integration_failed",
Expand Down
1 change: 1 addition & 0 deletions gui/src/pages/integrations/overview-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface ClaudeCodePayload {
authMode?: string;
}
export interface ClaudeDesktopPayload {
enabled?: boolean;
applied?: boolean;
stale?: boolean;
activeProfile?: boolean | null;
Expand Down
42 changes: 41 additions & 1 deletion src/claude/desktop-3p.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createHash, randomUUID } from "node:crypto";
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { atomicWriteFile } from "../config";
Expand Down Expand Up @@ -379,3 +379,43 @@ export function atomicReplaceDesktopConfig(
writer(path, content);
return existsSync(backupPath) ? { backupPath } : {};
}

/**
* Remove the opencodex entry from Claude Desktop's 3P config library.
*
* Deletes the UUID-named config JSON and strips the entry from _meta.json.
* Safe to call when the library directory or metadata does not exist — it
* returns `cleared: false` rather than throwing.
*/
export function clearDesktop3pConfig(): { cleared: boolean; path: string; reason?: string } {
const libraryPath = resolveDesktop3pConfigLibraryPath();
const metadataPath = join(libraryPath, "_meta.json");
const sentinel = join(libraryPath, "_meta.json");
try {
if (!existsSync(metadataPath)) {
return { cleared: false, path: sentinel, reason: "no metadata" };
}
const metadata = parseMetadata(metadataPath);
const entry = metadata.entries.find(e => e?.name === "opencodex" && typeof e.id === "string");
if (!entry || typeof entry.id !== "string") {
return { cleared: false, path: sentinel, reason: "no opencodex entry" };
}
const configPath = join(libraryPath, `${entry.id}.json`);
// Remove the config file — best-effort; a missing file is not a failure.
if (existsSync(configPath)) {
unlinkSync(configPath);
Comment on lines +399 to +406

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate the metadata entry ID before constructing the deletion path.

parseMetadata accepts arbitrary entry objects. Lines 399-406 only require entry.id to be a string. A crafted value such as "../../target" escapes libraryPath through join(...) and lets unlinkSync remove a reachable target.json file.

Require a canonical UUID before using an entry ID. Apply the validation in a shared metadata helper so writeDesktop3pConfig also cannot read or write through a malicious existing ID.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/claude/desktop-3p.ts` around lines 399 - 406, Validate the selected
opencodex entry ID with the project’s canonical UUID validation in a shared
metadata helper before constructing any path. Update both the deletion flow
around parseMetadata and writeDesktop3pConfig to reject non-UUID IDs, preventing
join-based path traversal while preserving the existing invalid-entry handling.

}
// Strip the entry from metadata.
const entries = metadata.entries.filter(e => e !== entry);
const nextMeta: Desktop3pMetadata = { ...metadata, entries };
// If Desktop is currently serving our profile, clear the appliedId too.
if (nextMeta.appliedId === entry.id) {
delete nextMeta.appliedId;
}
atomicWriteFile(metadataPath, JSON.stringify(nextMeta, null, 2) + "\n");
return { cleared: true, path: configPath };
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
return { cleared: false, path: sentinel, reason };
}
}
3 changes: 2 additions & 1 deletion src/server/management/agent-settings-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
try {
const state = await buildClaudeDesktopState(config);
const runtimePort = Number(url.port) || config.port;
return jsonResponse({ ...state, port: runtimePort });
return jsonResponse({ ...state, port: runtimePort, enabled: config.claudeCode?.desktopEnabled !== false });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Gate every Claude Desktop configuration writer with desktopEnabled.

The response at Line 665 reports that routing is disabled, but autoApplyDesktopBestEffort at Lines 130-150 and POST /api/claude-desktop/apply at Lines 701-754 still call writeDesktop3pConfig.

After a user disables routing, a catalog update or direct apply request can recreate the Desktop 3P entry. Both paths also pass config.apiKeys?.[0]?.key to the writer.

Return before auto-apply when config.claudeCode?.desktopEnabled === false. Reject apply requests while disabled. Allow only the native toggle enable path to restore the 3P configuration.

As per path instructions, “tokens and OAuth material must never be logged or serialized into responses.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/management/agent-settings-routes.ts` at line 665, Gate every
Claude Desktop configuration writer on config.claudeCode?.desktopEnabled: return
before writeDesktop3pConfig in autoApplyDesktopBestEffort and reject POST
/api/claude-desktop/apply while disabled. Preserve restoration only through the
native toggle-enable path. Ensure tokens and OAuth material, including the API
key passed to the writer, are never logged or serialized in responses.

Source: Path instructions

} catch (error) {
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
}
Expand Down Expand Up @@ -809,6 +809,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
stale,
activeProfile,
health,
enabled: config.claudeCode?.desktopEnabled !== false,
});
} catch (error) {
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
Expand Down
Loading
Loading