feat(app): mask secrets in API key and MCP install snippets with a shared RevealSnippet - #2988
Conversation
… snippets Introduce a shared RevealSnippet compound component that masks secrets in code snippets until revealed, while copy always yields the real value. Adopt it across the Team Settings API-key fields and the MCP install snippets (CLI command, deep-link JSON fallback, plain JSON). Masking is driven entirely by the secrets prop: omitted/empty renders a plain snippet, a bare string masks the whole value via defaultRedact, and [real, redacted] pairs mask specific substrings.
🦋 Changeset detectedLatest commit: 9f403bb The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🟡 Tier 3 — StandardIntroduces new logic, modifies core functionality, or touches areas with non-trivial risk. Why this tier:
Review process: Full human review — logic, architecture, edge cases. Stats
|
Greptile SummaryThe PR adds a shared reveal-aware snippet component and adopts it for Team Settings credentials and MCP installation snippets.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/app/src/components/RevealSnippet/RevealSnippet.tsx | Introduces the compound masking, reveal, input, and copy primitive; the previous key-rotation issue is fixed by resetting reveal state when the value changes. |
| packages/app/src/components/ClickStackOnboarding/McpInstallPanel.tsx | Passes the deployment access key through every visible MCP host snippet so credentials are masked consistently. |
| packages/app/src/components/ClickStackOnboarding/CopySnippet.tsx | Uses RevealSnippet for credential-bearing snippets while retaining the existing plain-snippet path. |
| packages/app/src/components/TeamSettings/ApiKeysSection.tsx | Replaces plaintext key controls with masked read-only fields containing reveal and copy actions. |
| packages/app/src/components/RevealSnippet/tests/RevealSnippet.test.tsx | Covers default masking, reveal behavior, real-value copying, nullable secrets, and re-masking after value changes. |
Reviews (3): Last reviewed commit: "Merge branch 'main' into brandon/reveal-..." | Re-trigger Greptile
Deep Review✅ No critical issues found. The previously flagged P1 (reveal state surviving key rotation) is resolved in this branch — 🟡 P2 — recommended
🔵 P3 nitpicks (1)
Reviewers (7): correctness, testing, maintainability, project-standards, kieran-typescript, security, previous-comments. Testing gaps:
Notes: The Cursor/VS Code deep links carry the base64/URL-encoded key in the DOM |
E2E Test Results✅ All tests passed • 307 passed • 1 skipped • 1191s
Tests ran across 4 shards in parallel. |
- Reset reveal state when the value changes (e.g. API key rotation) so a freshly issued credential is never shown without a fresh reveal (P1). - De-export defaultRedact, SecretInput, RevealRenderProps (knip: unused exports); they remain internal. - Drop an `as HTMLInputElement` cast in the ApiKeysSection test in favor of the exact masked-string assertion (which already covers length).
…daction - Add tests asserting Copy and Input place the REAL value on the clipboard while the snippet is still masked (the core copy-without-reveal promise). - Add McpInstallPanel consumer tests: the access key is masked in both the command and JSON-fallback snippets, and revealed only on an explicit action. - Clamp defaultRedact's visible prefix to half the value so short inputs are still masked instead of shown in full. - Redact longest secrets first so overlapping real values are order-independent. - Re-export defaultRedact and use it in the ApiKeysSection test instead of a local re-implementation. - Document the intended residual: MCP deep links carry the (encoded) key in their href by necessity; only the JSON fallback is reveal-gated.
…ted context The reviewer had no access to this repo's conventions at all. Verified with a canary rather than assumed: with `--setting-sources user` the model receives neither CLAUDE.md nor AGENTS.md; without the flag it receives AGENTS.md via the `@AGENTS.md` import but still not agent_docs/code_style.md, because AGENTS.md links that file rather than @-importing it. So the conventions never reached the model by any path. Dropping the flag is not an option: the working tree is fork-authored and that flag is what stops a PR injecting instructions into a job holding ANTHROPIC_API_KEY and a write token. So the conventions are fed in from the base-pinned trusted checkout instead. - agent_docs/code_style.md is added to the trusted sparse-checkout. Explicitly: this checkout is non-cone (it was set that way for .nvmrc), so root and nested files do not come along for free. - `Materialize review context` appends a `## Repository conventions` section holding that file, deliberately OUTSIDE the untrusted fence and labelled authoritative, since it comes from the base branch rather than the PR. The prompt's trust boundary gains a matching carve-out, so the reviewer does not distrust it along with everything else in the checkout. - The prompt gains a `Project conventions` pass. Feeding a rules document nobody is asked to apply would have changed nothing. - trusted-hash.sh covers the conventions file, so editing the rules invalidates the gate. That does mean a rules change re-reviews every open PR, which is the right trade for a change in what "correct" means. AGENTS.md is deliberately NOT included. It is 19.5KB of process guidance -- dev setup, running tests, changelog generation, merge conflicts -- with very little a reviewer can act on, and adding it would roughly double the token cost for that. code_style.md is 17KB of directly reviewable rules, several marked REQUIRED, and maps onto misses measured on the eval set: an icon-only ActionIcon with no accessible label (#1609) and a hand-rolled control that should have used a Mantine variant (#2988) are both REQUIRED sections of it. Two caveats. This is unevaluated: the harness passes the same `--setting-sources user`, so the 40% figure was measured without conventions too -- this is a new variable, not a restored one, and prior rounds showed added context is not free. And a PR that edits the conventions is still reviewed against the base copy, which is correct for trust and slightly wrong for that PR. 35 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What & why
Credentials were rendered in plain text across the app — the Team Settings API-key cards showed the full key, and the MCP install snippets inlined the personal access key directly into the copyable command/JSON. That's a shoulder-surfing risk on a screen-share or in a screenshot.
This adds a shared
RevealSnippetcomponent that masks secrets in a snippet until the user explicitly reveals them, while copy always hands back the real value (so nobody has to reveal a key just to copy it). It's adopted across every credential surface touched here:Design
RevealSnippetis a compound component —RevealSnippet.Code,.Reveal,.Copy, and a self-contained.Inputshare reveal state via context, so the surrounding layout (header row, panel chrome, button placement) is entirely up to the caller.Masking is driven by a single
secretsprop, with a deliberately safe default:secrets[][value](bare string)defaultRedact(prefix + dots) — for a bare API key[[real, redacted], …]Bare-string and pair entries can be mixed, and
nullentries are skipped sosecrets={[maybeKey]}is safe (e.g. a ClickHouse Cloud deployment with no key). The masked stand-in is padded to the real length so the field width doesn't jump on reveal.Screenshots
Testing
RevealSnippet(masking, reveal toggle, bare-string vs. pair vs. null secrets,canReveal=false, render-prop override) and updatedApiKeysSectiontests (masked value on the input, inline reveal).tsc --noEmit, ESLint, and unit tests all pass.