Skip to content

feat(privacy): mask account emails across all display surfaces - #164

Merged
ndycode merged 5 commits into
mainfrom
feat/163-consistent-account-email-masking
Jun 5, 2026
Merged

ndycode merged 5 commits into
mainfrom
feat/163-consistent-account-email-masking

Conversation

@ndycode

@ndycode ndycode commented Jun 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes #163.

maskEmail previously only reached the TUI prompt quota status. Every other account-display surface (account menu, command output, runtime messages, standalone login menu) still rendered full emails, so private addresses could leak into screenshots, screen sharing, and terminal recordings.

This change extends the existing maskEmail flag so it applies consistently to all human-facing account displays. No new config keys are introduced. When maskEmail is enabled, emails are reduced to a domain-preserving masked form (for example us***@example.com); account labels set via codex-label are always preferred over emails.

What changed

  • New lib/account-display.ts with maskEmailForDisplay (domain-preserving) and resolveDisplayEmail helpers as a single source of truth.
  • Threaded maskEmail through the account-display formatters:
    • formatAccountLabel (lib/accounts.ts) used by runtime and rotation messages.
    • formatCommandAccountLabel (index.ts) and its tool call sites: codex-list, codex-status, codex-limits, codex-health, codex-dashboard, codex-switch, codex-label, codex-tag, codex-note, codex-remove.
    • accountTitle in the interactive auth menu (lib/ui/auth-menu.ts) plus its detail and delete/refresh confirmation prompts.
    • formatAccountLabel in the standalone CLI login menu (lib/cli.ts).
    • The check-all and verify-flagged command output in index.ts, now label-first with a masked email fallback.
  • Added resolveMaskEmail to ToolContext so each tool resolves the flag once per invocation rather than per account.
  • Documentation updated in docs/configuration.md and docs/privacy.md.

Privacy boundary

  • Raw emails are still emitted only in opt-in --includeSensitive JSON output. In codex-health and codex-limits the sensitive JSON label field keeps the raw value while the rendered text uses the masked label, so --includeSensitive behavior is unchanged.
  • Default behavior is unchanged: maskEmail defaults to false, so emails render exactly as before unless a user opts in.

Verification

  • npm run typecheck: clean.
  • npm run lint: clean.
  • npm test: 86 files, 2412 passed, 1 skipped.
  • npm run build: succeeds.
  • New tests: test/account-display.test.ts, plus masking cases in test/accounts.test.ts, test/auth-menu.test.ts, and test/cli.test.ts.

Notes

  • No version bump in this PR; release is handled separately.
  • The TUI quota masking path from feat: add TUI email masking #160 is intentionally untouched; this PR brings the remaining surfaces to parity.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Account email masking now applies consistently across status screens, command output, interactive menus, and login flows; configured stable labels remain visible and raw emails appear only in opt-in machine-readable outputs.
  • Documentation

    • Expanded docs and privacy guidance on configuring email masking and reducing email exposure in screenshots/TUI sessions; environment variable/docs updated to reflect broader masking behavior.
  • Tests

    • Added tests verifying masked vs. unmasked account label behavior across interfaces.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr extends the existing maskEmail flag from the tui quota status to every human-facing account-display surface — command output, interactive menus, standalone login menu, and runtime rotation/failure messages — by centralizing masking logic in a new lib/account-display.ts module and threading resolveMaskEmail() through the ToolContext.

  • new lib/account-display.ts: maskEmailForDisplay (domain-preserving, handles single-char local parts and invalid emails) and resolveDisplayEmail as the single source of truth; all call sites now delegate here instead of rolling their own email trimming.
  • coverage: test/tools-codex-refresh.test.ts and test/tools-codex-remove.test.ts added; test/index.test.ts gains a full textSurfaces regression matrix plus interactive-picker and runtime-path guard tests; codex-limits and codex-dashboard masking paths are not yet in the matrix.
  • privacy boundary preserved: raw email still flows through to --includeSensitive json output via a separate unmasked label variable in codex-health and codex-limits; default behavior unchanged (maskEmail remains false).

Confidence Score: 5/5

safe to merge — masking is display-only and defaults to false, so existing behavior is unchanged unless the user opts in.

the change is well-scoped: a single new helper module, a consistent option threaded through every call site, and the privacy boundary preserved by keeping a separate unmasked label variable in the tools that need it. the regression suite is comprehensive for all but two tool surfaces.

test/index.test.ts — codex-limits and codex-dashboard are not in the textSurfaces masking matrix.

Important Files Changed

Filename Overview
lib/account-display.ts new shared masking helpers — maskEmailForDisplay and resolveDisplayEmail — clean, well-tested, correct edge-case handling for single-char locals, missing @, and empty input.
index.ts resolveMaskEmail closure added to ToolContext; promptAccountIndexSelection now masks the interactive picker labels; runtime paths (auth-failure, rate-limit toast, workspace-deactivated) wired through maskEmailEnabled.
lib/tools/codex-health.ts dual-label approach correct — raw label kept for --includeSensitive JSON, displayLabel used for all rendered text output.
lib/tools/codex-limits.ts same dual-label pattern as codex-health — raw label for JSON, displayLabel for text; masking test coverage not yet added to the surface matrix in test/index.test.ts.
lib/tools/codex-dashboard.ts resolveMaskEmail destructured and { maskEmail } passed to both formatCommandAccountLabel calls; no dedicated masking test yet.
lib/tools/codex-refresh.ts masking threaded correctly; test/tools-codex-refresh.test.ts added with masked and raw cases using a faithful formatCommandAccountLabel stand-in.
lib/tools/codex-remove.ts displayEmail used for the duplicate-entries hint guard and message; semantics preserved — falsy only when original account.email was empty/undefined.
test/index.test.ts comprehensive masking regression suite added; textSurfaces matrix missing codex-limits and codex-dashboard; runtime auth-failure path now has a spy-based guard test.
test/account-display.test.ts thorough unit tests for maskEmailForDisplay and resolveDisplayEmail, covering single-char local, multi-part domain, non-email fallback, empty/whitespace, and whitespace trimming.

Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
test/index.test.ts:1166-1227
**masking coverage gap for `codex-limits` and `codex-dashboard`**

the `textSurfaces` matrix covers `codex-list`, `codex-status`, `codex-health`, `codex-switch`, `codex-label`, `codex-tag`, `codex-note`, and `codex-remove` — but not `codex-limits` or `codex-dashboard`, both of which received `{ maskEmail }` threading in this pr. if the `resolveMaskEmail()` call or the `{ maskEmail }` option is accidentally dropped from either tool's execute body, no test will catch it. `codex-health` is already in the matrix and needed the same pattern, so adding both tools here is straightforward.

Reviews (5): Last reviewed commit: "test(privacy): broaden #163 coverage acr..." | Re-trigger Greptile

Extend the existing maskEmail config so it applies consistently to every
human-facing account display, not just the TUI quota status. Previously
maskEmail only reached the quota path while the account menu, command
output, runtime messages, and standalone login menu still rendered full
emails, exposing private addresses in screenshots and screen shares.

- Add lib/account-display.ts with maskEmailForDisplay (domain-preserving,
  e.g. us***@example.com) and resolveDisplayEmail helpers.
- Thread maskEmail through formatAccountLabel (lib/accounts.ts),
  formatCommandAccountLabel and its tool call sites, the interactive auth
  menu (accountTitle), and the standalone CLI login menu.
- Prefer account labels and mask the email fallback in the check-all and
  verify-flagged command output.
- Keep raw emails in opt-in --includeSensitive JSON output unchanged.
- Document the broadened behavior in configuration.md and privacy.md.

Closes #163
@coderabbitai

coderabbitai Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Add shared email-masking helpers, expose resolveMaskEmail on ToolContext, wire maskEmail through index/CLI/UI and all codex-* tools, update tests, and broaden docs to describe masking across account-display surfaces.

Changes

Email masking across account displays

Layer / File(s) Summary
Core helpers & contract
lib/account-display.ts, lib/accounts.ts, index.ts
Add maskEmailForDisplay() and resolveDisplayEmail(), extend formatCommandAccountLabel(..., options?: { maskEmail?: boolean }), and expose resolveMaskEmail() on the shared ToolContext.
Index wiring
index.ts
Derive maskEmail from plugin config via getCodexTuiMaskEmail, compute resolveMaskEmail(), and propagate maskEmail into interactive prompts and fallback label resolution paths.
CLI & Auth menu
lib/cli.ts, lib/ui/auth-menu.ts
Add maskEmail?: boolean option to login/menu flows, pass mask setting into showAuthMenu/showAccountDetails, and use resolveDisplayEmail when composing account titles and confirmation prompts.
Tool integrations
lib/tools/*
Each codex-* tool pulls resolveMaskEmail from ToolContext, computes maskEmail at runtime, and passes { maskEmail } into formatCommandAccountLabel so all UI/text outputs consistently respect masking; JSON --includeSensitive behavior unchanged.
codex-remove specifics
lib/tools/codex-remove.ts
Use resolveDisplayEmail for the duplicate-entry hint and pass mask option into removed-entry label formatting across v2 and non-v2 branches.
Tests
test/*
Add unit tests for masking helpers, update account/CLI/auth-menu tests, add integration/regression tests (issue #163), and add tool-specific test suites verifying masked vs raw output when maskEmail toggled.
Docs & privacy
docs/configuration.md, docs/privacy.md
Broaden maskEmail and CODEX_TUI_MASK_EMAIL docs to state masking applies across TUI quota status, command outputs, interactive account menu, and standalone login menu; add a privacy subsection describing how to avoid leaking emails and mention --includeSensitive opt-in raw JSON output.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

"🐰 I hop and tuck each email tight,
masking bright in day and night,
labels first, then stars in place,
privacy hopped into every space. 🥕"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(privacy): mask account emails across all display surfaces' directly and specifically summarizes the main change: extending email masking to all account display surfaces, not just quota status.
Description check ✅ Passed The description covers all template sections: it clearly states what changed and why (closes #163, extends maskEmail to all surfaces), includes comprehensive testing checklist completion, and confirms compliance with scope and authentication requirements.
Linked Issues check ✅ Passed The PR fully addresses issue #163 objectives: (1) extends maskEmail flag consistently across all display surfaces [account menu, command output, runtime messages, login menu], (2) maintains label preference over emails, (3) applies domain-preserving masking as fallback, and (4) preserves raw-email emission only in opt-in --includeSensitive JSON output.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to implementing issue #163 requirements: new account-display helpers, threaded maskEmail parameter through formatters and tools, updated documentation, and comprehensive test coverage. No unrelated refactoring or feature additions present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/163-consistent-account-email-masking

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread docs/configuration.md Resolved
Comment thread lib/tools/codex-refresh.ts Resolved
Comment thread index.ts Resolved

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/tools/codex-remove.ts (1)

213-217: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Post-remove hint still exposes raw email

The follow-up hint uses account.email directly, so masking can be bypassed in normal output even though the main label is masked.

Suggested fix
- const postRemoveHint =
-   matchingEmailRemaining > 0 && account.email
-     ? formatUiItem(
-         ui,
-         `Other entries for ${account.email} remain: ${matchingEmailRemaining}`,
-         "muted",
-       )
+ const postRemoveHint =
+   matchingEmailRemaining > 0
+     ? formatUiItem(
+         ui,
+         `Other entries for this account identity remain: ${matchingEmailRemaining}`,
+         "muted",
+       )
      : formatUiItem(
          ui,
          "Only the selected entry was removed.",
          "muted",
        );
...
- const postRemoveHint =
-   matchingEmailRemaining > 0 && account.email
-     ? `Other entries for ${account.email} remain: ${matchingEmailRemaining}`
+ const postRemoveHint =
+   matchingEmailRemaining > 0
+     ? `Other entries for this account identity remain: ${matchingEmailRemaining}`
      : "Only the selected entry was removed.";

As per coding guidelines, lib/**/*.ts: “Do not expose account emails, access tokens, refresh tokens, or raw prompt/response bodies in normal diagnostics”.

Also applies to: 243-245

🤖 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 `@lib/tools/codex-remove.ts` around lines 213 - 217, The follow-up hint is
exposing raw emails by using account.email directly in formatUiItem; update the
calls (the one around matchingEmailRemaining and the similar occurrences at
lines ~243-245) to pass a masked email instead (e.g., maskEmail(account.email)
or an existing mask utility) before calling formatUiItem so diagnostics never
include raw account.email; modify where formatUiItem is invoked and ensure the
masking function handles null/undefined safely.
index.ts (1)

949-950: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Interactive picker/setup labels still bypass maskEmail

promptAccountIndexSelection and toBeginnerAccountSnapshots call formatCommandAccountLabel without { maskEmail }, so these human-facing paths can still show raw emails when masking is enabled.

Suggested fix
 const promptAccountIndexSelection = async (
   ui: UiRuntimeOptions,
   storage: AccountStorageV3,
   title: string,
 ): Promise<number | null> => {
+  const maskEmail = resolveMaskEmail();
   if (!supportsInteractiveMenus()) return null;
   try {
     const { select } = await import("./lib/ui/select.js");
     const selected = await select<number>(
       storage.accounts.map((account, index) => ({
-        label: formatCommandAccountLabel(account, index),
+        label: formatCommandAccountLabel(account, index, { maskEmail }),
         value: index,
       })),
 const toBeginnerAccountSnapshots = (
   storage: AccountStorageV3,
   activeIndex: number,
   now: number,
 ): BeginnerAccountSnapshot[] => {
+  const maskEmail = resolveMaskEmail();
   return storage.accounts.map((account, index) => ({
     index,
-    label: formatCommandAccountLabel(account, index),
+    label: formatCommandAccountLabel(account, index, { maskEmail }),

Also applies to: 974-974

🤖 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 `@index.ts` around lines 949 - 950, Calls to formatCommandAccountLabel in
promptAccountIndexSelection and toBeginnerAccountSnapshots are missing the
maskEmail option so masked-email mode can be bypassed; update both call sites to
pass the maskEmail flag (e.g., formatCommandAccountLabel(account, index, {
maskEmail })) sourcing maskEmail from the surrounding options/args/context (or
add it to the function parameters if not available) so the label creation honors
masking consistently.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@docs/configuration.md`:
- Around line 162-163: Update the docs so the env-var documentation for
CODEX_TUI_MASK_EMAIL (and the related maskEmailInQuotaDetails setting) matches
the expanded behavior described under maskEmail: change the line that currently
limits CODEX_TUI_MASK_EMAIL to "prompt quota status" to state it controls
masking across all account-display surfaces (TUI prompt quota status, command
outputs like codex-list/codex-status/.../codex-remove, interactive account menu,
and standalone login menu), and ensure maskEmailInQuotaDetails is documented as
additionally toggling active account masking inside the quota details dialog;
reference the config keys maskEmail and maskEmailInQuotaDetails and the env-var
CODEX_TUI_MASK_EMAIL so the two sections are consistent.

In `@test/account-display.test.ts`:
- Line 1: Remove the explicit import line "import { describe, expect, it } from
'vitest';" from test/account-display.test.ts and rely on the Vitest globals
describe, it, and expect defined by the test runner; update the file to call
describe/it/expect directly (no import) and ensure the test uses the existing
global symbols throughout (look for usages of describe, it, expect in this
file).

---

Outside diff comments:
In `@index.ts`:
- Around line 949-950: Calls to formatCommandAccountLabel in
promptAccountIndexSelection and toBeginnerAccountSnapshots are missing the
maskEmail option so masked-email mode can be bypassed; update both call sites to
pass the maskEmail flag (e.g., formatCommandAccountLabel(account, index, {
maskEmail })) sourcing maskEmail from the surrounding options/args/context (or
add it to the function parameters if not available) so the label creation honors
masking consistently.

In `@lib/tools/codex-remove.ts`:
- Around line 213-217: The follow-up hint is exposing raw emails by using
account.email directly in formatUiItem; update the calls (the one around
matchingEmailRemaining and the similar occurrences at lines ~243-245) to pass a
masked email instead (e.g., maskEmail(account.email) or an existing mask
utility) before calling formatUiItem so diagnostics never include raw
account.email; modify where formatUiItem is invoked and ensure the masking
function handles null/undefined safely.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f355dfde-b952-47c2-8d74-4aaf6b5457c5

📥 Commits

Reviewing files that changed from the base of the PR and between 22e7823 and 194249d.

📒 Files selected for processing (24)
  • docs/configuration.md
  • docs/privacy.md
  • index.ts
  • lib/account-display.ts
  • lib/accounts.ts
  • lib/cli.ts
  • lib/tools/codex-dashboard.ts
  • lib/tools/codex-health.ts
  • lib/tools/codex-label.ts
  • lib/tools/codex-limits.ts
  • lib/tools/codex-list.ts
  • lib/tools/codex-note.ts
  • lib/tools/codex-refresh.ts
  • lib/tools/codex-remove.ts
  • lib/tools/codex-status.ts
  • lib/tools/codex-switch.ts
  • lib/tools/codex-tag.ts
  • lib/tools/index.ts
  • lib/ui/auth-menu.ts
  • test/account-display.test.ts
  • test/accounts.test.ts
  • test/auth-menu.test.ts
  • test/cli.test.ts
  • test/index.test.ts

Comment thread docs/configuration.md Resolved
Comment thread test/account-display.test.ts Resolved
ndycode and others added 4 commits June 4, 2026 21:12
Address review feedback on PR #164:
- List codex-refresh among the maskEmail-affected command surfaces.
- Update the CODEX_TUI_MASK_EMAIL env-var description so it no longer
  contradicts the broadened maskEmail behavior documented above it.
- Add test/tools-codex-refresh.test.ts covering masked and raw output
  so a dropped maskEmail argument at the refresh call site is caught.
Address remaining review feedback on PR #164:
- Make the accounts.js formatAccountLabel mock a spyable vi.fn and add a
  test asserting the auth-failure removal path builds the account label
  with { maskEmail: true }. Verified it fails if the option is dropped
  from the call site, so the three runtime sites are now guarded.
- Make getCodexTuiMaskEmail mockable per test (vi.fn) so the masking
  path can be toggled without affecting other tests.
- Drop the explicit vitest import from account-display.test.ts now that
  the file relies on the configured globals.
PR #163/#164 threaded maskEmail through most label formatters but four
human-facing surfaces still printed raw emails when maskEmail is enabled,
defeating the masking on exactly the screenshot/screen-share threat model
the feature targets:

- index.ts promptAccountIndexSelection: the interactive account picker used
  by codex-switch/remove/label/tag/note rendered raw emails (no { maskEmail }).
- lib/tools/codex-remove.ts: the post-remove "Other entries for X remain"
  hint (both v2 and plain branches) interpolated the raw account.email even
  though the main label above it was masked.
- index.ts interactive manage menu: the delete confirmation and the
  enable/disable toggle confirmation console.log'd the raw target.email.

All four now route through resolveDisplayEmail with the resolved maskEmail
flag. Default (maskEmail=false) behavior is unchanged.

Regression tests:
- test/tools-codex-remove.test.ts (new): asserts the duplicate-entries hint
  masks/unmasks correctly.
- test/index.test.ts: drive the interactive delete and toggle confirmation
  paths (previously zero coverage), asserting masked output and no raw email.

Verified: build, typecheck, lint clean; 2419 tests pass (+4), 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an issue-#163 regression suite that drives the REAL plugin tools (and
thus the real `formatCommandAccountLabel` closure), so a dropped
`{ maskEmail }` at any call site fails here rather than slipping through.

Invariant under test: when maskEmail is enabled, no human-facing surface
renders a raw email; when disabled, the raw email still renders (backward
compatible).

Coverage:
- Parametrized masked-on / raw-off across codex-list, codex-status,
  codex-health, codex-switch, codex-label, codex-tag, codex-note, codex-remove.
- Multi-account listing masks every email.
- codex-remove duplicate-entries hint masks/unmasks (guards the leak fixed
  in 24ce61d).
- Interactive account picker (promptAccountIndexSelection) masks menu labels
  (guards the originally-flagged leak) — drives the real closure via a
  captured select() mock with a stubbed TTY.
- --includeSensitive JSON still emits the raw email (privacy boundary intact).

Each new assertion was mutation-verified: reverting the corresponding fix
makes the test fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ndycode
ndycode merged commit 2094bf0 into main Jun 5, 2026
1 of 2 checks passed
ndycode added a commit that referenced this pull request Jun 6, 2026
Minor release on top of 6.2.0: email masking across all display surfaces (#164), 16 deep-audit bug fixes (#165), and the deep stress suite (#166). Bumps package.json, .release-please-manifest.json, and .codex-plugin/plugin.json to 6.3.0. 2487 tests pass; build/lint/typecheck clean; publish dry-run verified.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add privacy-safe account labels across account switcher and command displays

1 participant