diff --git a/CHANGELOG.md b/CHANGELOG.md
index a1d2ed3f74..9405dd6241 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@
### Added
- Grok: Settings and tray **Add account** flow matching Codex/Claude — isolated `grok login --oauth`, save current CLI login, switch, and remove without logging out the active session.
+### Fixed
+- Claude: when Hide Personal Info is enabled, keep saved account rows distinguishable with stable localized `Account N` labels and matching redacted tooltips.
+
---
## [Windows] 0.60.3 - 2026-09-15
diff --git a/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx
index b403a87540..a3c4e558e4 100644
--- a/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx
+++ b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx
@@ -1,4 +1,4 @@
-import { act, fireEvent, render, screen } from "@testing-library/react";
+import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ClaudeAccount } from "../types/bridge";
@@ -39,12 +39,31 @@ describe("ClaudeAccountsMenu", () => {
expect(mocks.claudeAccountSwitch).toHaveBeenCalledWith(second.id);
});
- it("masks emails, including tooltips, when hideEmail is enabled", async () => {
+ it("uses stable opaque account labels and redacts tooltips when hideEmail is enabled", async () => {
mocks.claudeAccountsList.mockResolvedValue([first, { ...second, organization: `${second.email}'s Organization` }]);
const { container } = render();
await screen.findByText("ClaudeAccountsTitle");
+ const labels = container.querySelectorAll(".codex-menu-accounts__email");
+ expect(labels[0].firstChild?.textContent).toBe("Account 1");
+ expect(labels[0].getAttribute("title")).toBe("Account 1");
+ expect(labels[1].firstChild?.textContent).toBe("Account 2");
+ expect(labels[1].getAttribute("title")).toBe("Account 2");
expect(container.textContent).not.toContain(first.email);
expect(container.innerHTML).not.toContain(second.email);
+ expect(container.textContent).not.toContain("Personal");
+ expect(container.textContent).not.toContain("Organization");
+ });
+
+ it("keeps opaque labels stable when the source reorders accounts", async () => {
+ const { container } = render();
+ await screen.findByText("ClaudeAccountsTitle");
+ mocks.claudeAccountsList.mockResolvedValue([second, first]);
+ await act(async () => window.dispatchEvent(new Event("focus")));
+ await waitFor(() => {
+ const labels = container.querySelectorAll(".codex-menu-accounts__email");
+ expect(labels[0].firstChild?.textContent).toBe("Account 2");
+ expect(labels[1].firstChild?.textContent).toBe("Account 1");
+ });
});
it("shows switch failures and leaves the current account marked active", async () => {
diff --git a/apps/desktop-tauri/src/components/ClaudeAccountsMenu.tsx b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.tsx
index eccc12b2b5..c15e78ebe3 100644
--- a/apps/desktop-tauri/src/components/ClaudeAccountsMenu.tsx
+++ b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.tsx
@@ -3,7 +3,10 @@ import { listen } from "@tauri-apps/api/event";
import type { ClaudeAccount } from "../types/bridge";
import { claudeAccountsList, claudeAccountSwitch } from "../lib/tauri";
import { useLocale } from "../hooks/useLocale";
-import { maskEmail } from "./MenuCard";
+import {
+ buildClaudeAccountOrdinals,
+ buildPrivateClaudeAccountLabel,
+} from "./claudeAccountDisplay";
export default function ClaudeAccountsMenu({ hideEmail, onLayoutChange }: {
hideEmail: boolean;
@@ -40,6 +43,8 @@ export default function ClaudeAccountsMenu({ hideEmail, onLayoutChange }: {
onLayoutChange?.();
}, [accounts.length, error, switched, onLayoutChange]);
+ const accountOrdinals = buildClaudeAccountOrdinals(accounts);
+
const switchAccount = async (id: string) => {
setBusy(true);
setError(null);
@@ -67,13 +72,18 @@ export default function ClaudeAccountsMenu({ hideEmail, onLayoutChange }: {
{switched &&
{t("ClaudeAccountsSwitched")}
}
{accounts.map(account => {
- const email = hideEmail ? maskEmail(account.email) : account.email;
+ const privateLabel = buildPrivateClaudeAccountLabel(
+ account,
+ accountOrdinals[account.id],
+ hideEmail,
+ t("Account"),
+ );
return (
-
-
- {email}
+
+ {privateLabel.label}
{account.isActive && {t("TokenAccountActive")}}
{!hideEmail && account.organization && !account.organization.includes(account.email) && (
diff --git a/apps/desktop-tauri/src/components/claudeAccountDisplay.ts b/apps/desktop-tauri/src/components/claudeAccountDisplay.ts
new file mode 100644
index 0000000000..a6ee39ae4c
--- /dev/null
+++ b/apps/desktop-tauri/src/components/claudeAccountDisplay.ts
@@ -0,0 +1,34 @@
+import type { ClaudeAccount } from "../types/bridge";
+
+export interface PrivateClaudeAccountLabel {
+ label: string;
+ tooltip: string;
+}
+
+/**
+ * Assign opaque labels from stable account ids rather than the current array
+ * order. The account list can be reordered when the active login changes, but
+ * the saved Claude account id remains source-owned and stable.
+ */
+export function buildClaudeAccountOrdinals(
+ accounts: readonly ClaudeAccount[],
+): Record {
+ const ids = [...new Set(accounts.map((account) => account.id))].sort((left, right) =>
+ left < right ? -1 : left > right ? 1 : 0,
+ );
+ return Object.fromEntries(ids.map((id, index) => [id, index + 1]));
+}
+
+export function buildPrivateClaudeAccountLabel(
+ account: ClaudeAccount,
+ ordinal: number | undefined,
+ hidePersonalInfo: boolean,
+ accountWord: string,
+): PrivateClaudeAccountLabel {
+ if (hidePersonalInfo) {
+ const label = ordinal === undefined ? "••••" : `${accountWord} ${ordinal}`;
+ return { label, tooltip: label };
+ }
+
+ return { label: account.email, tooltip: account.email };
+}