Skip to content

feat(app): mask secrets in API key and MCP install snippets with a shared RevealSnippet - #2988

Merged
kodiakhq[bot] merged 4 commits into
mainfrom
brandon/reveal-snippet-component
Aug 25, 2026
Merged

feat(app): mask secrets in API key and MCP install snippets with a shared RevealSnippet#2988
kodiakhq[bot] merged 4 commits into
mainfrom
brandon/reveal-snippet-component

Conversation

@brandon-pereira

@brandon-pereira brandon-pereira commented Aug 24, 2026

Copy link
Copy Markdown
Member

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 RevealSnippet component 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:

  • Team Settings — ingestion API key + personal access key now render as masked, read-only fields with an inline reveal-eye and copy icon.
  • MCP install panel — the access key is masked in every host snippet (Claude Code / Codex CLI commands, the Cursor & VS Code deep-link JSON fallback, and the generic JSON block).

Design

RevealSnippet is a compound componentRevealSnippet.Code, .Reveal, .Copy, and a self-contained .Input share 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 secrets prop, with a deliberately safe default:

secrets behavior
omitted / [] nothing masked — plain copyable snippet
[value] (bare string) whole value masked via defaultRedact (prefix + dots) — for a bare API key
[[real, redacted], …] only those substrings masked — for a secret embedded in a config/prompt

Bare-string and pair entries can be mixed, and null entries are skipped so secrets={[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

Screenshot 2026-08-24 at 3 09 14 PM

Testing

  • New unit tests for RevealSnippet (masking, reveal toggle, bare-string vs. pair vs. null secrets, canReveal=false, render-prop override) and updated ApiKeysSection tests (masked value on the input, inline reveal).
  • Storybook stories cover every capability (API-key field, single/multiple secrets, MCP install shapes, custom reveal control, reveal-disabled, nullable secret, no secrets).
  • tsc --noEmit, ESLint, and unit tests all pass.

… 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-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9f403bb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

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

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 25, 2026 2:51pm
hyperdx-storybook Ready Ready Preview Aug 25, 2026 2:51pm

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Aug 24, 2026
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 573 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 7
  • Production lines changed: 573 (+ 721 in test files, excluded from tier calculation)
  • Branch: brandon/reveal-snippet-component
  • Author: brandon-pereira

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a shared reveal-aware snippet component and adopts it for Team Settings credentials and MCP installation snippets.

  • Masks API keys and embedded MCP credentials until explicitly revealed.
  • Preserves copying of the real credential while the displayed value remains masked.
  • Adds unit tests and Storybook coverage for masking, reveal, copy, nullable secrets, and credential rotation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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

Comment thread packages/app/src/components/RevealSnippet/RevealSnippet.tsx
Comment thread packages/app/src/components/RevealSnippet/RevealSnippet.tsx Outdated
Comment thread packages/app/src/components/RevealSnippet/RevealSnippet.tsx
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found. The previously flagged P1 (reveal state surviving key rotation) is resolved in this branch — RevealSnippet re-masks during render when value changes (RevealSnippet.tsx), with a dedicated regression test. Copy-while-masked, canReveal=false, null secrets, and bare-string vs. pair secrets are all covered. The items below are hardening and standards recommendations only.

🟡 P2 — recommended

  • packages/app/src/components/RevealSnippet/RevealSnippet.tsx:91redact masks by exact substring match on the raw secret, but the Codex snippet passes the key through shellEscape and the JSON blocks through JSON.stringify before rendering, so a key containing ", \, $, or a backtick would appear in escaped form that the raw-value match never masks; not reachable with today's UUIDv4 keys, but the masking guarantee silently depends on that format assumption.
    • Fix: Redact the same rendered representations the snippet builders emit (e.g. also register the shell-escaped and JSON-escaped forms of each secret as redaction targets), and add a test asserting a metacharacter-bearing key stays masked.
    • security, correctness, previous-comments
🔵 P3 nitpicks (1)
  • packages/app/src/components/RevealSnippet/RevealSnippet.tsx:359 — The file is 359 lines, over the documented 300-line component limit in AGENTS.md:92 ("Keep files under 300 lines; break down large components").
    • Fix: Extract the compound subcomponents (Code, Reveal, Input, Copy) or the redaction helpers into sibling modules to bring the file under the limit.
    • project-standards, maintainability, previous-comments

Reviewers (7): correctness, testing, maintainability, project-standards, kieran-typescript, security, previous-comments.

Testing gaps:

  • No test asserts masking holds for an access key containing shell/JSON metacharacters — the escaped-form gap above is currently unverified either way.

Notes: The Cursor/VS Code deep links carry the base64/URL-encoded key in the DOM href and are intentionally left unmasked (documented as residual-by-design in installSnippets.ts); it is not human-readable shoulder-surfable text and is required for one-click install, so it is not treated as a finding. The knip "unused export" flag on defaultRedact is a false positive — it is imported by two test files for assertions; SecretInput and RevealRenderProps are already non-exported in the current diff.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 307 passed • 1 skipped • 1191s

Status Count
✅ Passed 307
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

- 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.
Comment thread packages/app/src/components/RevealSnippet/RevealSnippet.module.scss
Comment thread packages/app/src/components/RevealSnippet/RevealSnippet.tsx
@kodiakhq
kodiakhq Bot merged commit e995c39 into main Aug 25, 2026
41 of 42 checks passed
@kodiakhq
kodiakhq Bot deleted the brandon/reveal-snippet-component branch August 25, 2026 15:01
MikeShi42 added a commit that referenced this pull request Aug 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants