feat: add Tool Search domain restriction bypass with active-installation patching

Resolve the active `claude` command from PATH and apply an equal-length
byte patch to remove the domain whitelist check. Backups are stored in
~/.cc-switch/toolsearch-backups/ (SHA-256 of path) so they survive
Claude Code version upgrades. The patch auto-reapplies on app startup
when the setting is enabled.

Frontend checks PatchResult.success and rolls back the setting on failure.
This commit is contained in:
Jason
2026-03-14 21:20:05 +08:00
parent 7097a0d710
commit 9439153f05
13 changed files with 699 additions and 2 deletions
+9 -1
View File
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import type { SettingsFormState } from "@/hooks/useSettings";
import { AppWindow, MonitorUp, Power, EyeOff } from "lucide-react";
import { AppWindow, MonitorUp, Power, EyeOff, Search } from "lucide-react";
import { ToggleRow } from "@/components/ui/toggle-row";
import { AnimatePresence, motion } from "framer-motion";
@@ -66,6 +66,14 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {
onCheckedChange={(value) => onChange({ skipClaudeOnboarding: value })}
/>
<ToggleRow
icon={<Search className="h-4 w-4 text-amber-500" />}
title={t("settings.toolSearchBypass")}
description={t("settings.toolSearchBypassDescription")}
checked={!!settings.toolSearchBypass}
onCheckedChange={(value) => onChange({ toolSearchBypass: value })}
/>
<ToggleRow
icon={<AppWindow className="h-4 w-4 text-blue-500" />}
title={t("settings.minimizeToTray")}
+39 -1
View File
@@ -197,6 +197,44 @@ export function useSettings(): UseSettingsResult {
}
}
// Tool Search bypass: apply/restore patch when toggled
const nextToolSearchBypass = updates.toolSearchBypass;
if (
nextToolSearchBypass !== undefined &&
nextToolSearchBypass !== (data?.toolSearchBypass ?? false)
) {
try {
const results = nextToolSearchBypass
? await settingsApi.applyToolSearchPatch()
: await settingsApi.restoreToolSearchPatch();
const failed = results.find((r) => !r.success);
if (failed) {
throw new Error(failed.error ?? "Tool Search patch failed");
}
} catch (error) {
console.warn(
"[useSettings] Failed to sync Tool Search bypass",
error,
);
// Rollback: revert the setting we already saved
const rolledBack = {
...payload,
toolSearchBypass: !nextToolSearchBypass,
};
await saveMutation.mutateAsync(rolledBack);
updateSettings({ toolSearchBypass: !nextToolSearchBypass });
toast.error(
nextToolSearchBypass
? t("notifications.toolSearchPatchFailed", {
defaultValue: "Tool Search 补丁操作失败",
})
: t("notifications.toolSearchRestoreFailed", {
defaultValue: "Tool Search 恢复操作失败",
}),
);
}
}
// 持久化语言偏好
try {
if (typeof window !== "undefined" && updates.language) {
@@ -228,7 +266,7 @@ export function useSettings(): UseSettingsResult {
throw error;
}
},
[data, saveMutation, settings, t],
[data, saveMutation, settings, t, updateSettings],
);
// 完整保存设置(用于 Advanced 标签页的手动保存)
+3
View File
@@ -85,6 +85,7 @@ export function useSettingsForm(): UseSettingsFormResult {
data.enableClaudePluginIntegration ?? false,
silentStartup: data.silentStartup ?? false,
skipClaudeOnboarding: data.skipClaudeOnboarding ?? false,
toolSearchBypass: data.toolSearchBypass ?? false,
claudeConfigDir: sanitizeDir(data.claudeConfigDir),
codexConfigDir: sanitizeDir(data.codexConfigDir),
geminiConfigDir: sanitizeDir(data.geminiConfigDir),
@@ -107,6 +108,7 @@ export function useSettingsForm(): UseSettingsFormResult {
minimizeToTrayOnClose: true,
enableClaudePluginIntegration: false,
skipClaudeOnboarding: false,
toolSearchBypass: false,
language: readPersistedLanguage(),
} as SettingsFormState);
@@ -143,6 +145,7 @@ export function useSettingsForm(): UseSettingsFormResult {
serverData.enableClaudePluginIntegration ?? false,
silentStartup: serverData.silentStartup ?? false,
skipClaudeOnboarding: serverData.skipClaudeOnboarding ?? false,
toolSearchBypass: serverData.toolSearchBypass ?? false,
claudeConfigDir: sanitizeDir(serverData.claudeConfigDir),
codexConfigDir: sanitizeDir(serverData.codexConfigDir),
geminiConfigDir: sanitizeDir(serverData.geminiConfigDir),
+4
View File
@@ -166,6 +166,8 @@
"syncClaudePluginFailed": "Sync Claude plugin failed",
"skipClaudeOnboardingFailed": "Failed to skip Claude Code first-run confirmation",
"clearClaudeOnboardingSkipFailed": "Failed to restore Claude Code first-run confirmation",
"toolSearchPatchFailed": "Failed to apply Tool Search patch",
"toolSearchRestoreFailed": "Failed to restore Tool Search patch",
"updateSuccess": "Provider updated successfully",
"updateFailed": "Failed to update provider: {{error}}",
"deleteSuccess": "Provider deleted",
@@ -463,6 +465,8 @@
"enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app",
"skipClaudeOnboarding": "Skip Claude Code first-run confirmation",
"skipClaudeOnboardingDescription": "When enabled, Claude Code will skip the first-run confirmation",
"toolSearchBypass": "Bypass Tool Search domain restriction",
"toolSearchBypassDescription": "Remove the Tool Search domain whitelist in the active Claude Code installation. Auto-reapplied after that installation updates",
"appVisibility": {
"title": "Homepage Display",
"description": "Choose which apps to show on the homepage",
+4
View File
@@ -166,6 +166,8 @@
"syncClaudePluginFailed": "Claude プラグインとの同期に失敗しました",
"skipClaudeOnboardingFailed": "Claude Code の初回確認スキップに失敗しました",
"clearClaudeOnboardingSkipFailed": "Claude Code の初回確認の復元に失敗しました",
"toolSearchPatchFailed": "Tool Search パッチの適用に失敗しました",
"toolSearchRestoreFailed": "Tool Search パッチの復元に失敗しました",
"updateSuccess": "プロバイダーを更新しました",
"updateFailed": "プロバイダーの更新に失敗しました: {{error}}",
"deleteSuccess": "プロバイダーを削除しました",
@@ -463,6 +465,8 @@
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
"skipClaudeOnboardingDescription": "オンにすると Claude Code の初回インストール確認をスキップします",
"toolSearchBypass": "Tool Search ドメイン制限を解除",
"toolSearchBypassDescription": "現在アクティブな Claude Code インストールの Tool Search ドメインホワイトリスト制限を解除します。そのインストールの更新後は自動で再適用されます",
"appVisibility": {
"title": "ホームページ表示",
"description": "ホームページに表示するアプリを選択",
+4
View File
@@ -166,6 +166,8 @@
"syncClaudePluginFailed": "同步 Claude 插件失败",
"skipClaudeOnboardingFailed": "跳过 Claude Code 初次安装确认失败",
"clearClaudeOnboardingSkipFailed": "恢复 Claude Code 初次安装确认失败",
"toolSearchPatchFailed": "Tool Search 补丁操作失败",
"toolSearchRestoreFailed": "Tool Search 恢复操作失败",
"updateSuccess": "供应商更新成功",
"updateFailed": "更新供应商失败:{{error}}",
"deleteSuccess": "供应商已删除",
@@ -463,6 +465,8 @@
"enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换",
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
"skipClaudeOnboardingDescription": "开启后跳过 Claude Code 初次安装确认",
"toolSearchBypass": "解除 Tool Search 域名限制",
"toolSearchBypassDescription": "解除当前活跃 Claude Code 安装的 Tool Search 域名白名单限制。该安装更新后会自动重新应用",
"appVisibility": {
"title": "主页面显示",
"description": "选择在主页面显示的应用",
+12
View File
@@ -86,6 +86,18 @@ export const settingsApi = {
return await invoke("clear_claude_onboarding_skip");
},
async applyToolSearchPatch(): Promise<
Array<{ path: string; success: boolean; error?: string }>
> {
return await invoke("apply_toolsearch_patch");
},
async restoreToolSearchPatch(): Promise<
Array<{ path: string; success: boolean; error?: string }>
> {
return await invoke("restore_toolsearch_patch");
},
async saveFileDialog(defaultName: string): Promise<string | null> {
return await invoke("save_file_dialog", { defaultName });
},
+2
View File
@@ -226,6 +226,8 @@ export interface Settings {
enableClaudePluginIntegration?: boolean;
// 跳过 Claude Code 初次安装确认(写入 ~/.claude.json 的 hasCompletedOnboarding
skipClaudeOnboarding?: boolean;
// 解除 Tool Search 域名限制
toolSearchBypass?: boolean;
// 是否开机自启
launchOnStartup?: boolean;
// 静默启动(程序启动时不显示主窗口)