Skip to content

Commit a939c6b

Browse files
authored
feat: add TUI email masking (#160)
Opt-in TUI email masking via maskEmail/maskEmailInQuotaDetails config and CODEX_TUI_MASK_EMAIL/CODEX_TUI_MASK_EMAIL_DETAILS env vars. Masks account hints in prompt quota status and quota details; defaults false. Docs + tests + .gitignore reorg.
1 parent 5a28241 commit a939c6b

10 files changed

Lines changed: 273 additions & 26 deletions

File tree

.gitignore

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,79 @@
1+
# Dependencies
12
node_modules/
23
bun.lockb
34
pnpm-lock.yaml
5+
6+
# Environment / secrets
7+
.env
8+
.env.local
9+
credentials.json
10+
state.json
11+
12+
# Build artifacts
413
dist/
14+
build/
15+
*.tgz
16+
*.patched
17+
18+
# macOS / Apple sidecar files
19+
._*
520
.DS_Store
6-
.history/
21+
.AppleDouble
22+
.LSOverride
23+
24+
# OpenCode local config and cache
725
opencode.json
826
.opencode/
9-
tmp
10-
tmp*
11-
*.tgz
12-
*.patched
13-
*first_fail*
14-
Heap-*.heapsnapshot
1527
.codex-cache
1628
.sisyphus/
17-
coverage/
18-
winston/
29+
30+
# Skills
31+
.agents/
32+
skills-lock.json
33+
34+
# IDE
35+
.history/
36+
.vscode/
37+
.idea/
38+
.shard/
39+
40+
# Local Docker Compose overrides
41+
docker-compose.yaml
42+
docker-compose.local.yml
43+
docker-compose.local.yaml
44+
compose.local.yml
45+
compose.local.yaml
46+
47+
# Temporary files
48+
tmp
49+
tmp*
1950
.tmp-plugin-test/
2051
.tmp-release-*.md
52+
53+
# Debug logs
54+
debug_logs*/
55+
debug*.json
56+
winston/
57+
Heap-*.heapsnapshot
58+
*first_fail*
59+
60+
# Project-specific notes and scratch files
61+
_notes/
62+
requests/
2163
task_plan.md
2264
findings.md
2365
progress.md
66+
67+
# Testing and coverage
68+
coverage/
69+
.pytest_cache/
70+
.coverage
71+
htmlcov/
72+
73+
# Python
74+
__pycache__/
75+
*.py[cod]
76+
*$py.class
77+
*.so
78+
.Python
79+
*.egg-info/

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ Selected runtime/environment overrides:
249249
| `CODEX_TUI_V2=0/1` | Disable/enable codex-style tool output |
250250
| `CODEX_TUI_COLOR_PROFILE=truecolor|ansi256|ansi16` | Force terminal color profile |
251251
| `CODEX_TUI_GLYPHS=ascii|unicode|auto` | Force terminal glyph style |
252+
| `CODEX_TUI_MASK_EMAIL=0/1` | Hide account email in TUI quota status only |
253+
| `CODEX_TUI_MASK_EMAIL_DETAILS=0/1` | Also hide account email in quota details when prompt masking is enabled |
252254
| `CODEX_AUTH_PER_PROJECT_ACCOUNTS=0/1` | Disable/enable per-project account pools |
253255
| `CODEX_AUTH_AUTO_UPDATE=0/1` | Disable/enable daily npm update check and cache refresh |
254256
| `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=strict|fallback` | Control unsupported-model retry behavior |

docs/configuration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ advanced settings go in `~/.opencode/openai-codex-auth-config.json`:
121121
"codexTuiV2": true,
122122
"codexTuiColorProfile": "truecolor",
123123
"codexTuiGlyphMode": "ascii",
124+
"maskEmail": false,
125+
"maskEmailInQuotaDetails": false,
124126
"beginnerSafeMode": false,
125127
"fastSession": false,
126128
"fastSessionStrategy": "hybrid",
@@ -157,6 +159,8 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound
157159
| `codexTuiV2` | `true` | enables codex-style terminal ui output (set `false` to keep legacy output) |
158160
| `codexTuiColorProfile` | `truecolor` | terminal color profile for codex ui (`truecolor`, `ansi256`, `ansi16`) |
159161
| `codexTuiGlyphMode` | `ascii` | glyph set for codex ui (`ascii`, `unicode`, `auto`) |
162+
| `maskEmail` | `false` | masks the active account email in the TUI prompt quota status only |
163+
| `maskEmailInQuotaDetails` | `false` | also masks the active account email in the quota details dialog when `maskEmail` is enabled |
160164
| `beginnerSafeMode` | `false` | enables conservative beginner-safe runtime behavior for retries and recovery |
161165
| `fastSession` | `false` | forces low-latency settings per request (`reasoningEffort=none/low`, `reasoningSummary=auto`, `textVerbosity=low`) |
162166
| `fastSessionStrategy` | `hybrid` | `hybrid` speeds simple turns and keeps full-depth for complex prompts; `always` forces fast mode every turn |
@@ -241,6 +245,8 @@ override any config with env vars:
241245
| `CODEX_TUI_V2=0` | disable codex-style ui (use legacy output) |
242246
| `CODEX_TUI_COLOR_PROFILE=ansi16` | force color profile for codex ui |
243247
| `CODEX_TUI_GLYPHS=unicode` | override glyph mode (`ascii`, `unicode`, `auto`) |
248+
| `CODEX_TUI_MASK_EMAIL=1` | mask the active account email in the TUI prompt quota status only |
249+
| `CODEX_TUI_MASK_EMAIL_DETAILS=1` | also mask the active account email in quota details when prompt masking is enabled |
244250
| `CODEX_AUTH_PREWARM=0` | disable startup prewarm (prompt/instruction cache warmup) |
245251
| `CODEX_AUTH_FAST_SESSION=1` | enable fast-session defaults |
246252
| `CODEX_AUTH_FAST_SESSION_STRATEGY=always` | force fast mode on every prompt |

docs/development/TUI_PARITY_CHECKLIST.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Use this checklist to keep `oc-codex-multi-auth` aligned with the Antigravity-st
7878
- Visual controls:
7979
- `codexTuiColorProfile`: `truecolor` / `ansi256` / `ansi16`
8080
- `codexTuiGlyphMode`: `ascii` / `unicode` / `auto`
81+
- Privacy controls: `maskEmail: true` or `CODEX_TUI_MASK_EMAIL=1` hides account email in prompt quota status only; `maskEmailInQuotaDetails: true` or `CODEX_TUI_MASK_EMAIL_DETAILS=1` extends masking to quota details.
8182

8283
## Tooling Parity
8384

lib/config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const DEFAULT_CONFIG: PluginConfig = {
3535
codexTuiV2: true,
3636
codexTuiColorProfile: "truecolor",
3737
codexTuiGlyphMode: "ascii",
38+
maskEmail: false,
39+
maskEmailInQuotaDetails: false,
3840
beginnerSafeMode: false,
3941
fastSession: false,
4042
fastSessionStrategy: "hybrid",
@@ -283,6 +285,24 @@ export function getCodexTuiGlyphMode(
283285
);
284286
}
285287

288+
export function getCodexTuiMaskEmail(pluginConfig: PluginConfig): boolean {
289+
return resolveBooleanSetting(
290+
"CODEX_TUI_MASK_EMAIL",
291+
pluginConfig.maskEmail,
292+
false,
293+
);
294+
}
295+
296+
export function getCodexTuiMaskEmailInQuotaDetails(
297+
pluginConfig: PluginConfig,
298+
): boolean {
299+
return resolveBooleanSetting(
300+
"CODEX_TUI_MASK_EMAIL_DETAILS",
301+
pluginConfig.maskEmailInQuotaDetails,
302+
false,
303+
);
304+
}
305+
286306
export function getFastSession(pluginConfig: PluginConfig): boolean {
287307
return resolveBooleanSetting(
288308
"CODEX_AUTH_FAST_SESSION",

lib/schemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export const PluginConfigSchema = z.object({
1616
codexTuiV2: z.boolean().optional(),
1717
codexTuiColorProfile: z.enum(["truecolor", "ansi16", "ansi256"]).optional(),
1818
codexTuiGlyphMode: z.enum(["ascii", "unicode", "auto"]).optional(),
19+
maskEmail: z.boolean().optional(),
20+
maskEmailInQuotaDetails: z.boolean().optional(),
1921
beginnerSafeMode: z.boolean().optional(),
2022
fastSession: z.boolean().optional(),
2123
fastSessionStrategy: z.enum(["hybrid", "always"]).optional(),

lib/tui-status.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ const variantSuffixes: ReasoningVariant[] = [
5656
const STATUS_SEPARATOR = ` ${String.fromCharCode(183)} `;
5757
const WARNING_LIMIT_LEFT_PERCENT = 25;
5858
const DANGER_LIMIT_LEFT_PERCENT = 10;
59+
const MASKED_EMAIL = "*****";
60+
const EMAIL_PATTERN = /[^\s(),<>]+@[^\s(),<>]+/;
61+
const EMAIL_PATTERN_GLOBAL = /[^\s(),<>]+@[^\s(),<>]+/g;
5962

6063
function isRecord(value: unknown): value is Record<string, unknown> {
6164
return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -190,13 +193,24 @@ function isPercent(value: number | null): value is number {
190193
}
191194

192195
function extractEmailFromLabel(label: string | undefined): string | undefined {
193-
const match = label?.match(/[^\s(),<>]+@[^\s(),<>]+/);
196+
const match = label?.match(EMAIL_PATTERN);
194197
return match?.[0];
195198
}
196199

197-
function formatAccountEmail(email: string | undefined): string | undefined {
200+
function formatAccountEmail(
201+
email: string | undefined,
202+
maskEmail: boolean,
203+
): string | undefined {
198204
const trimmed = email?.trim() || undefined;
199-
return trimmed ? `[${trimmed}]` : undefined;
205+
return trimmed ? `[${maskEmail ? MASKED_EMAIL : trimmed}]` : undefined;
206+
}
207+
208+
function maskEmailsInText(
209+
value: string | undefined,
210+
maskEmail: boolean,
211+
): string | undefined {
212+
if (!value) return undefined;
213+
return maskEmail ? value.replace(EMAIL_PATTERN_GLOBAL, MASKED_EMAIL) : value;
200214
}
201215

202216
function formatQuotaLimit(
@@ -212,7 +226,10 @@ function formatQuotaLimit(
212226
return reset ? `${base} resets ${reset}` : base;
213227
}
214228

215-
function formatAccountHint(quota: CompactQuotaStatus): string | undefined {
229+
function formatAccountHint(
230+
quota: CompactQuotaStatus,
231+
maskEmail = false,
232+
): string | undefined {
216233
if (quota.type !== "ready") return undefined;
217234
if (
218235
typeof quota.accountIndex !== "number" ||
@@ -228,8 +245,8 @@ function formatAccountHint(quota: CompactQuotaStatus): string | undefined {
228245
return undefined;
229246
}
230247
const email =
231-
formatAccountEmail(quota.accountEmail) ??
232-
formatAccountEmail(extractEmailFromLabel(quota.accountLabel));
248+
formatAccountEmail(quota.accountEmail, maskEmail) ??
249+
formatAccountEmail(extractEmailFromLabel(quota.accountLabel), maskEmail);
233250
if (email) return email;
234251
return `A${quota.accountIndex}`;
235252
}
@@ -286,9 +303,10 @@ export function formatPromptStatusText(params: {
286303
variant?: ReasoningVariant;
287304
quota: CompactQuotaStatus;
288305
width?: number;
306+
maskEmail?: boolean;
289307
}): string {
290308
const variant = params.variant;
291-
const account = formatAccountHint(params.quota);
309+
const account = formatAccountHint(params.quota, params.maskEmail);
292310
const quotaParts = formatQuotaParts(params.quota, true);
293311
const quotaPartsWithoutReset = formatQuotaParts(params.quota, false);
294312
const quota = quotaParts.length > 0
@@ -403,17 +421,19 @@ function formatDetailsLimit(limit: CompactQuotaLimit): string {
403421
export function formatQuotaDetailsText(
404422
quota: CompactQuotaStatus,
405423
now = Date.now(),
424+
options: { maskEmail?: boolean } = {},
406425
): string {
407426
if (quota.type === "loading") return "Quota is loading.";
408427
if (quota.type === "missing") return "No Codex OAuth account is configured.";
409428
if (quota.type === "unavailable") return "Quota is unavailable.";
410429

411430
const lines: string[] = [];
412-
const accountHint = formatAccountHint(quota);
413-
if (quota.accountLabel && accountHint) {
414-
lines.push(`Account: ${accountHint} (${quota.accountLabel})`);
415-
} else if (quota.accountLabel) {
416-
lines.push(`Account: ${quota.accountLabel}`);
431+
const accountHint = formatAccountHint(quota, options.maskEmail);
432+
const accountLabel = maskEmailsInText(quota.accountLabel, Boolean(options.maskEmail));
433+
if (accountLabel && accountHint) {
434+
lines.push(`Account: ${accountHint} (${accountLabel})`);
435+
} else if (accountLabel) {
436+
lines.push(`Account: ${accountLabel}`);
417437
} else if (accountHint) {
418438
lines.push(`Account: ${accountHint}`);
419439
}

test/plugin-config.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
getCodexTuiV2,
66
getCodexTuiColorProfile,
77
getCodexTuiGlyphMode,
8+
getCodexTuiMaskEmail,
9+
getCodexTuiMaskEmailInQuotaDetails,
810
getBeginnerSafeMode,
911
getFastSession,
1012
getFastSessionStrategy,
@@ -55,6 +57,8 @@ describe('Plugin Configuration', () => {
5557
'CODEX_TUI_V2',
5658
'CODEX_TUI_COLOR_PROFILE',
5759
'CODEX_TUI_GLYPHS',
60+
'CODEX_TUI_MASK_EMAIL',
61+
'CODEX_TUI_MASK_EMAIL_DETAILS',
5862
'CODEX_AUTH_FAST_SESSION',
5963
'CODEX_AUTH_BEGINNER_SAFE_MODE',
6064
'CODEX_AUTH_FAST_SESSION_STRATEGY',
@@ -98,6 +102,8 @@ describe('Plugin Configuration', () => {
98102
codexTuiV2: true,
99103
codexTuiColorProfile: 'truecolor',
100104
codexTuiGlyphMode: 'ascii',
105+
maskEmail: false,
106+
maskEmailInQuotaDetails: false,
101107
beginnerSafeMode: false,
102108
fastSession: false,
103109
fastSessionStrategy: 'hybrid',
@@ -143,6 +149,8 @@ describe('Plugin Configuration', () => {
143149
codexTuiV2: true,
144150
codexTuiColorProfile: 'truecolor',
145151
codexTuiGlyphMode: 'ascii',
152+
maskEmail: false,
153+
maskEmailInQuotaDetails: false,
146154
beginnerSafeMode: false,
147155
fastSession: false,
148156
fastSessionStrategy: 'hybrid',
@@ -185,6 +193,8 @@ describe('Plugin Configuration', () => {
185193
codexTuiV2: true,
186194
codexTuiColorProfile: 'truecolor',
187195
codexTuiGlyphMode: 'ascii',
196+
maskEmail: false,
197+
maskEmailInQuotaDetails: false,
188198
beginnerSafeMode: false,
189199
fastSession: false,
190200
fastSessionStrategy: 'hybrid',
@@ -238,6 +248,8 @@ describe('Plugin Configuration', () => {
238248
codexTuiV2: true,
239249
codexTuiColorProfile: 'truecolor',
240250
codexTuiGlyphMode: 'ascii',
251+
maskEmail: false,
252+
maskEmailInQuotaDetails: false,
241253
beginnerSafeMode: false,
242254
fastSession: false,
243255
fastSessionStrategy: 'hybrid',
@@ -285,6 +297,8 @@ describe('Plugin Configuration', () => {
285297
codexTuiV2: true,
286298
codexTuiColorProfile: 'truecolor',
287299
codexTuiGlyphMode: 'ascii',
300+
maskEmail: false,
301+
maskEmailInQuotaDetails: false,
288302
beginnerSafeMode: false,
289303
fastSession: false,
290304
fastSessionStrategy: 'hybrid',
@@ -438,6 +452,50 @@ describe('Plugin Configuration', () => {
438452
});
439453
});
440454

455+
describe('getCodexTuiMaskEmail', () => {
456+
it('should default to false', () => {
457+
delete process.env.CODEX_TUI_MASK_EMAIL;
458+
expect(getCodexTuiMaskEmail({})).toBe(false);
459+
});
460+
461+
it('should use config value when env var not set', () => {
462+
delete process.env.CODEX_TUI_MASK_EMAIL;
463+
expect(getCodexTuiMaskEmail({ maskEmail: true })).toBe(true);
464+
});
465+
466+
it('should prioritize env value over config', () => {
467+
process.env.CODEX_TUI_MASK_EMAIL = '0';
468+
expect(getCodexTuiMaskEmail({ maskEmail: true })).toBe(false);
469+
process.env.CODEX_TUI_MASK_EMAIL = '1';
470+
expect(getCodexTuiMaskEmail({ maskEmail: false })).toBe(true);
471+
});
472+
});
473+
474+
describe('getCodexTuiMaskEmailInQuotaDetails', () => {
475+
it('should default to false', () => {
476+
delete process.env.CODEX_TUI_MASK_EMAIL_DETAILS;
477+
expect(getCodexTuiMaskEmailInQuotaDetails({})).toBe(false);
478+
});
479+
480+
it('should use config value when env var not set', () => {
481+
delete process.env.CODEX_TUI_MASK_EMAIL_DETAILS;
482+
expect(
483+
getCodexTuiMaskEmailInQuotaDetails({ maskEmailInQuotaDetails: true }),
484+
).toBe(true);
485+
});
486+
487+
it('should prioritize env value over config', () => {
488+
process.env.CODEX_TUI_MASK_EMAIL_DETAILS = '0';
489+
expect(
490+
getCodexTuiMaskEmailInQuotaDetails({ maskEmailInQuotaDetails: true }),
491+
).toBe(false);
492+
process.env.CODEX_TUI_MASK_EMAIL_DETAILS = '1';
493+
expect(
494+
getCodexTuiMaskEmailInQuotaDetails({ maskEmailInQuotaDetails: false }),
495+
).toBe(true);
496+
});
497+
});
498+
441499
describe('getFastSession', () => {
442500
it('should default to false', () => {
443501
delete process.env.CODEX_AUTH_FAST_SESSION;
@@ -766,4 +824,3 @@ describe('Plugin Configuration', () => {
766824
});
767825
});
768826
});
769-

0 commit comments

Comments
 (0)