Skip to content

Sentry: scrub file paths / PII / secrets from events before send (beforeSend/beforeBreadcrumb) - #5598

Merged
lawrencecchen merged 20 commits into
mainfrom
feat-sentry-scrub
Jun 8, 2026
Merged

lawrencecchen merged 20 commits into
mainfrom
feat-sentry-scrub

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

cmux's Sentry config (Sources/AppDelegate.swift, CLI/cmux.swift) sends to hosted sentry.io (third-party US service) with sendDefaultPii = false but no content scrubber, so error messages, exception values, breadcrumbs, request/transaction data, the user context, and stack-frame file paths went out unscrubbed. That leaked local paths like /Users/<name>/... (revealing the username), project/workspace names, URLs, and possibly command/secret text.

This adds a beforeSend + beforeBreadcrumb (+ beforeSendSpan) content scrubber to both Sentry inits.

Coverage contract

The pure SentryScrubber (in CmuxFoundation, no Sentry dependency, unit-tested) redacts on every string:

  • Home/user paths: the real home dir, plus any /Users/<name>/ or /home/<name>/ prefix. The generic rule also covers build-machine stack-frame paths whose home differs from the runtime one.
  • Emails.
  • Secrets: Bearer …, sk-/pk-/ghp_/xoxb- keys, JWTs, AWS access key IDs, and <sensitive-key>=value assignments (token/password/secret/api_key/access_key/auth/cookie/credential/session_id/private_key/bearer, including embedded env names like AWS_SECRET_ACCESS_KEY=). The key marker set is shared between the free-text regex and the dictionary path so a credential is caught whether it arrives as raw text or a dict key.
  • Dictionaries/headers: values under a sensitive key are dropped wholesale regardless of shape (string, array, nested dict). URL/NSURL and other non-scalar values are stringified and scrubbed (Sentry serializes them to their description after beforeSend); genuine scalars (numbers, bools, dates, Data, NSNull) pass through.

The contract is scrub every reachable free-text field; preserve only an explicit grouping allowlist, so a new free-text field is covered by default.

SentryEventScrubber (app + CLI target, where Sentry is linked) applies it to: event message.formatted (rebuilt — capture(message:) populates formatted and leaves the template nil), transaction, serverName, logger; exception value; exception mechanism desc/helpLink/data (where capture(error:) copies NSError.userInfo); every stack frame's fileName/package/contextLine/preContext/postContext/vars across exception, thread, and event stack traces; thread name; debug-image codeFile; request url/queryString/fragment/headers (cookies dropped); tags/extra/context; breadcrumb message/data; and child performance span spanDescription/data/tags. The user identity fields (userId/email/username/name/ipAddress/geo) are dropped wholesale.

Preserved for grouping (the allowlist): exception type, mechanism type, fingerprint, frame function/module/lineNumber/addresses, level, environment, releaseName, dist, modules.

Behavior change: macOS performance tracing disabled

The macOS app had tracesSampleRate = 0.1. The auto-instrumented root SentryTransaction.trace serializes its data/tags/description into the payload after beforeSend runs, and the root tracer is not reachable through the public Sentry API (it lives in the SDK's internal include/ headers), so it cannot be scrubbed. cmux does not consume these performance traces (no manual transactions or span.setData), so this PR sets tracesSampleRate = 0 on macOS to remove that un-scrubbable egress path. Crash, error, and app-hang reporting are unaffected (independent of the trace sample rate). The CLI already had tracesSampleRate = 0. The CLI also flips sendDefaultPii = true → false for defense-in-depth.

macOS + iOS

Both Sentry inits (macOS app and the bundled CLI) share one DSN and both got the scrubber. iOS ships no Sentry SDK at all (no import Sentry, no SentrySDK, no DSN anywhere under ios/), so it sends nothing to sentry.io and there is nothing to scrub there.

Tests

  • 35 SentryScrubber unit tests in CmuxFoundation (swift test --package-path Packages/CmuxFoundation): paths, emails, tokens, env-style and broad-marker assignments, key-aware dict redaction, URL/object stringification, scalar passthrough; plain text and grouping fields preserved.
  • SentryEventScrubberTests (cmuxTests) for the Event/Breadcrumb glue: formatted-only message, exception mechanism, frame paths/context/vars, thread frames, debugMeta, request/cookies, user-field drop, tags/extra/context.

Builds verified

macOS cmux scheme, cmux-cli, and cmux-ios simulator all build clean. Structured autoreview (Codex) is clean.

🤖 Generated with Claude Code


Note

Medium Risk
Touches all outbound Sentry payloads and changes macOS tracing sampling; scrubbing could over-redact debugging context or miss edge-case leaks despite broad tests.

Overview
Adds a defense-in-depth Sentry egress scrubber so crash/error telemetry to hosted Sentry no longer ships local paths, emails, tokens, and other PII in the clear.

CmuxFoundation gains a Sentry-free SentryScrubber (regex helpers, ScrubberDenylists aligned with sentry-python / relay) that redacts secrets, emails, /Users and /home paths, URL userinfo, key-aware dict/query handling, and wholesale drops for Data under sensitive keys.

SentryEventScrubber wires that into beforeSend, beforeBreadcrumb, and beforeSendSpan on the macOS app and CLI: scrub free-text event fields (messages, stack frame paths/lines/vars, request URL/query/headers, tags/extra/context, breadcrumbs) while preserving grouping fields (exception type, frame symbols, fingerprint). User identity and request cookies are cleared; CLI sendDefaultPii goes true → false.

macOS sets tracesSampleRate from 0.1 to 0 because root transaction payload fields are serialized after beforeSend and cannot be scrubbed via the public API. Crash/error/hang reporting is unchanged.

Extensive unit tests cover denylists, scrubber behavior, and event glue.

Reviewed by Cursor Bugbot for commit 4fa9ba7. Bugbot is set up for automated code reviews on this repo. Configure here.

Add a content scrubber to cmux's Sentry config (macOS app + CLI) so error
messages, exception values, breadcrumbs, request/transaction data, the user
context, and stack-frame file paths are redacted before they leave the device
for sentry.io.

The pure SentryScrubber (CmuxFoundation) redacts home/user paths (the real
home dir plus any /Users/<name>/ or /home/<name>/ prefix, which also covers
build-machine stack-frame paths), emails, and secrets (Bearer tokens, sk-/ghp-
style keys, JWTs, token=/password= assignments, AWS access keys). It leaves
grouping-relevant fields (exception type, fingerprint, frame function/module/
lineNumber) intact. SentryEventScrubber wires it into beforeSend/beforeBreadcrumb
and walks message.formatted, exception values, exception/thread/event stack
frames, request URL/query/cookies/headers, transaction, serverName, user
fields, tags, extra, and context.

The CLI previously set sendDefaultPii = true; flipped to false for
defense-in-depth (the scrubber also redacts any user fields that slip in).

iOS ships no Sentry SDK, so there is nothing to scrub there.

Tests: 24 SentryScrubber unit tests (CmuxFoundation, swift test) plus
SentryEventScrubberTests covering the Event/Breadcrumb glue including the
formatted-only message and context-dictionary leak paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 8, 2026 6:47am
cmux-staging Ready Ready Preview, Comment Jun 8, 2026 6:47am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

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

Adds a compiled regex helper and text scrubber, applies it to Sentry Event and Breadcrumb payloads to remove PII, and wires the scrubber into app startup and Sentry hooks.

Changes

Defense-in-Depth Sentry Event Scrubbing

Layer / File(s) Summary
Regex Pattern Foundation
Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift
New SentryRegexPattern wraps NSRegularExpression to enable match-aware text replacement with capture group extraction.
Text Scrubber Core Implementation
Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift
SentryScrubber redacts home paths, generic user path prefixes, emails, and secrets from text and JSON-like structures via ordered regex patterns and prefix-aware replacement.
Text Scrubber Test Coverage
Packages/CmuxFoundation/Tests/CmuxFoundationTests/SentryScrubberTests.swift
Deterministic tests verify path, email, secret/token redaction across multiple formats and recursive scrubbing of nested dictionaries/arrays while preserving non-sensitive values.
Sentry Event/Breadcrumb Scrubber Integration
Sources/SentryEventScrubber.swift
SentryEventScrubber rewrites Sentry Event and Breadcrumb payloads to redact message/transaction/exception values, frame paths, request fields, user identity, tags/extra/context while preserving grouping-relevant metadata.
Sentry Event/Breadcrumb Test Coverage
cmuxTests/SentryEventScrubberTests.swift
Tests exercise scrubbing across event shapes (messages, transactions, exceptions, frames, threads, requests, user, tags/extra, breadcrumbs) and confirm grouping fields/non-sensitive events remain unchanged.
Application Integration and Build Wiring
CLI/cmux.swift, Sources/AppDelegate.swift, cmux.xcodeproj/project.pbxproj
CLI disables sendDefaultPii and applies SentryEventScrubber to beforeSend/beforeBreadcrumb; AppDelegate initializes scrubbing; Xcode project updated to include new sources/tests and package product dependencies.

Sequence Diagram

sequenceDiagram
  participant Event as Sentry Event/Breadcrumb
  participant SentryEventScrubber as SentryEventScrubber
  participant SentryScrubber as SentryScrubber
  participant SentrySDK as Sentry SDK
  Event->>SentryEventScrubber: scrub(event) / scrub(breadcrumb)
  SentryEventScrubber->>SentryScrubber: scrub(message/path/email/token)
  SentryScrubber->>SentryScrubber: apply redaction patterns
  SentryScrubber-->>SentryEventScrubber: redacted text
  SentryEventScrubber->>SentryEventScrubber: rebuild event/breadcrumb
  SentryEventScrubber-->>SentrySDK: scrubbed event/breadcrumb
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4484: Related consolidation of config/path resolution and prior work touching application path helpers used by this scrubber integration.

Poem

🐰 I hop through logs with careful paws,

I tuck away emails, tokens, and claws.
Paths are softened, secrets put to bed,
The crash report hums — no PII is read.
🥕✨


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error SentryEventScrubber lacks actor isolation annotation and implicitly inherits @MainActor, but Sentry SDK calls its closures from background queues. Mark SentryEventScrubber as nonisolated struct since it's a pure value type with no MainActor dependencies, matching the documented concurrency contract.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Sentry event scrubbing for file paths, PII, and secrets via beforeSend/beforeBreadcrumb hooks.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Blocking Runtime ✅ Passed No blocking synchronization patterns (DispatchSemaphore, Thread.sleep, Task.sleep, .sync queues, locks) introduced. Code uses pure synchronous functions as Sendable value types.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code and Xcode project files; custom check scope is non-Swift app/runtime changes (TypeScript, JavaScript, shell, or non-Swift build scripts), which is not applicable here.
Cmux Algorithmic Complexity ✅ Passed Code processes bounded Sentry Event structures (exceptions, threads, frames, breadcrumbs), not user-owned scalable collections; not in hot paths; not batch-processing ~1000 records.
Cmux Swift Concurrency ✅ Passed No legacy async patterns found. New code uses pure Sendable value types wired via synchronous beforeSend/beforeBreadcrumb callbacks; no DispatchQueue.global().async, Combine, or fire-and-forget Tasks.
Cmux Swift @Concurrent ✅ Passed All new code is synchronous with proper Sendable conformance. Sentry closures are lightweight, pure transformations without async work or isolation issues.
Cmux Swift File And Package Boundaries ✅ Passed All new files under 400 lines with single responsibility; minimal additions to existing oversized files; pure logic extracted to CmuxFoundation package; SDK integration glue permitted in app target.
Cmux Swift Logging ✅ Passed No print/debugPrint/dump/NSLog calls added in production files. Scrubber is pure Swift with no logging. Sentry hooks properly configured via beforeSend/beforeBreadcrumb.
Cmux User-Facing Error Privacy ✅ Passed No user-facing error messages or alerts are added. Changes are internal telemetry scrubbing configuration for Sentry beforeSend/beforeBreadcrumb hooks.
Cmux Full Internationalization ✅ Passed Redaction placeholders are literal telemetry markers sent to sentry.io, not shown to end users, fitting allowed exceptions for literal tokens and developer-only debug logs per the rule.
Cmux Swiftui State Layout ✅ Passed PR contains only Sentry event scrubbing changes; no SwiftUI state, @Observable/@Published/@observableobject, GeometryReader, or render-time state mutations were added or modified.
Cmux Architecture Rethink ✅ Passed PR adds pure value scrubbers with synchronous data transformation for Sentry redaction; no timing/blocking patterns, shared state, observers, duplicate wiring, or lifecycle splits.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds Sentry event scrubbing only; does not create or modify any standalone windows, so the auxiliary window close shortcuts rule is not applicable.
Cmux Source Artifacts ✅ Passed All 8 changed Swift source/test files and Xcode project config are intentional hand-written code with no artifact patterns detected.
Description check ✅ Passed The PR description comprehensively covers the changes, rationale, testing approach, and behavioral impacts, following the template structure with detailed sections.
✨ 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-sentry-scrub

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d960ea197d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +143 to +144
for (key, value) in dictionary {
output[key] = scrub(value: value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact values based on sensitive dictionary keys

Because the dictionary scrubber ignores key and only scans the value string, structured Sentry payloads like scope.setContext(value: ["token": "abcdef0123456789secretvalue"], key: ...), extra, or breadcrumb data still send the raw secret when the value does not independently match a prefixed token/JWT/API-key regex. This directly affects the new event scrubber paths that pass context/extra/data through this method, so key names such as token, password, or api_key need to force the corresponding value to <redacted-secret>.

Useful? React with 👍 / 👎.

Comment thread Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift
@greptile-apps

greptile-apps Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires a defense-in-depth Sentry scrubbing pipeline into both the macOS app and CLI before any telemetry leaves the device, preventing local paths, emails, tokens, and user-identity fields from reaching hosted sentry.io.

  • SentryScrubber (Sentry-free, in CmuxFoundation) redacts free-text strings and recursive [String: Any] payloads using an upstream-pinned denylist (sentry-python + relay @common), then SentryEventScrubber (app + CLI targets) applies it to every sensitive field in Event, Breadcrumb, and Span objects while preserving grouping fields (exception type, frame symbols, fingerprint).
  • Config changes: CLI flips sendDefaultPii to false; macOS app sets tracesSampleRate = 0.0 because auto-instrumented root-span payload fields serialize after beforeSend and are unreachable through the public API.
  • The pure scrubber is covered by 35 unit tests in CmuxFoundation (no Sentry link required) plus SentryEventScrubberTests for the Sentry-type glue.

Confidence Score: 5/5

Safe to merge — scrubbing pipeline is comprehensive, all documented fields are covered, and the tracing-disable decision is clearly justified.

Every sensitive Sentry field documented in the PR contract is wired through the scrubber. The pure SentryScrubber is testable in isolation; 35 unit tests plus the event-glue suite give strong behavioral coverage. Regex patterns carry upstream provenance pins. The only open item is the captureGroup lower-bound guard (flagged in a prior thread), which has no current callsite passing 0.

No files require special attention.

Important Files Changed

Filename Overview
Sources/SentryEventScrubber.swift New file: glue that applies SentryScrubber to every sensitive Sentry type field. Covers all documented free-text fields (message, transaction, frames, mechanism, debugMeta, request, tags/extra/context, breadcrumbs, user). Grouping fields are explicitly preserved.
Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift New file: pure value transformer (no Sentry dependency). Handles URL-credential stripping, token/secret patterns, email, and home-path redaction in priority order. Recursive scrubber with key-aware boundary, Data-drop, and URL/object stringify path.
Packages/CmuxFoundation/Sources/CmuxFoundation/ScrubberDenylists.swift New file: provenance-pinned key-marker and value-regex denylists ported from sentry-python and relay @common, with upstream commit SHA citations.
Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift New file: thin NSRegularExpression wrapper with capture-aware reverse-iteration replacement. force-try is intentional (compile-time constants).
Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexMatch.swift New file: match wrapper exposing captureGroup(index:). The 1-based contract in the doc-comment is not enforced by a lower-bound guard — index=0 silently returns the full match text.
Sources/AppDelegate.swift Wires SentryEventScrubber into beforeSend/beforeBreadcrumb/beforeSendSpan and sets tracesSampleRate=0.0.
CLI/cmux.swift Flips sendDefaultPii from true to false and wires SentryEventScrubber into beforeSend/beforeBreadcrumb.
cmux.xcodeproj/project.pbxproj Adds SentryEventScrubber.swift to both app and CLI source lists, SentryEventScrubberTests.swift to cmuxTests, and adds Sentry + CmuxFoundation dependencies for the test target.

Sequence Diagram

sequenceDiagram
    participant App as AppDelegate / CLISocketSentryTelemetry
    participant SDK as Sentry SDK
    participant EScrubber as SentryEventScrubber
    participant SScrubber as SentryScrubber
    participant Sentry as sentry.io

    App->>SDK: SentrySDK.start(options)
    Note over App,SDK: registers beforeSend, beforeBreadcrumb, beforeSendSpan

    SDK->>EScrubber: beforeBreadcrumb(breadcrumb)
    EScrubber->>SScrubber: scrub message + data
    SScrubber-->>EScrubber: redacted values
    EScrubber-->>SDK: scrubbed breadcrumb

    SDK->>EScrubber: beforeSend(event)
    EScrubber->>SScrubber: scrub message, exception, frames, request, tags/extra/context
    EScrubber->>EScrubber: drop user identity fields
    SScrubber-->>EScrubber: redacted values
    EScrubber-->>SDK: scrubbed event

    SDK->>Sentry: POST scrubbed payload
Loading

Reviews (13): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

/// `tags`, `extra`, and breadcrumb `message` / `data` — while leaving
/// grouping-relevant fields (exception `type`, fingerprint, frame `function` /
/// `module` / `lineNumber`) untouched so Sentry issue grouping is unaffected.
struct SentryEventScrubber {

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.

P2 Missing Sendable conformance on captured type

SentryEventScrubber is captured by both beforeSend and beforeBreadcrumb closures, which the Sentry SDK calls from whatever dispatch queue produced the event — potentially crossing actor boundaries from the main-actor context where SentrySDK.start runs. In Swift 6 strict-concurrency mode this will produce a warning (or error) because the closure capture requires the value to be Sendable. Adding the conformance is safe: the single stored property is SentryScrubber which is already declared Sendable, so the struct synthesises it without changes.

Suggested change
struct SentryEventScrubber {
struct SentryEventScrubber: Sendable {

Comment thread Sources/SentryEventScrubber.swift Outdated
Comment on lines +142 to +144
user.email = scrubber.scrub(optional: user.email)
user.username = scrubber.scrub(optional: user.username)
user.name = scrubber.scrub(optional: user.name)

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.

P2 user.userId not scrubbed

The Sentry User type exposes a userId: String? field (distinct from username). The PR description lists the scrubbed user fields as "email/username/name/data, ipAddress dropped" but userId is absent. If cmux ever sets userId to an email address or a path-bearing value (e.g. via scope.setUser), it will leave the device unscrubbed. Given that the rest of the user object is being walked for defense-in-depth, routing userId through the scrubber keeps the coverage consistent.

Suggested change
user.email = scrubber.scrub(optional: user.email)
user.username = scrubber.scrub(optional: user.username)
user.name = scrubber.scrub(optional: user.name)
user.userId = scrubber.scrub(optional: user.userId)
user.email = scrubber.scrub(optional: user.email)
user.username = scrubber.scrub(optional: user.username)
user.name = scrubber.scrub(optional: user.name)

Comment on lines +31 to +32
public func captureGroup(_ index: Int) -> String? {
guard index < result.numberOfRanges else { return nil }

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.

P2 captureGroup guard doesn't enforce the documented 1-based contract

The doc-comment declares the parameter as a "1-based capture group index", but the guard index < result.numberOfRanges lets index = 0 through — which returns the text of the entire match rather than nil. While captureGroup(0) is never called today, the mismatch between contract and implementation is a maintenance hazard: a future caller following the docs would expect nil and receive unexpected match text. Adding an explicit lower-bound guard makes the 1-based promise machine-checked.

Suggested change
public func captureGroup(_ index: Int) -> String? {
guard index < result.numberOfRanges else { return nil }
public func captureGroup(_ index: Int) -> String? {
guard index >= 1, index < result.numberOfRanges else { return nil }

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 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 `@Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift`:
- Around line 51-54: The public initializer SentryRegexPattern.init currently
uses try! to create self.regex which can crash on invalid patterns; change the
API to either make the initializer throwing (declare public init(_ pattern:
String, options: NSRegularExpression.Options = [.caseInsensitive]) throws and
use try to construct NSRegularExpression then assign to regex, propagating the
error to callers) or, if external construction isn’t intended, reduce the
initializer’s access level (make it internal or fileprivate) and handle the
NSRegularExpression construction with a failable/guard path instead of try!;
update any callers of SentryRegexPattern.init to either catch/propagate the
thrown error or to use the new internal factory so the code no longer
force-unwraps NSRegularExpression.
- Around line 31-35: SentryRegexPattern.Match.captureGroup(_:) must validate
that index is within 0 ..< result.numberOfRanges (not just <) to avoid passing
negative indexes into NSTextCheckingResult.range(at:); add a guard that index >=
0 && index < result.numberOfRanges before calling range(at:) and return nil for
out-of-range values. For SentryRegexPattern.init(_:options:) remove the
force-try and either make the initializer throw (public init(...) throws) and
propagate NSRegularExpression initialization errors, or make the initializer
non-public if it must remain infallible; replace try! NSRegularExpression(...)
with a throwing call and update callers accordingly.

In `@Sources/SentryEventScrubber.swift`:
- Around line 40-46: serverName and user identity fields are being
pattern-scrubbed via scrubber.scrub(optional:) but must be fully redacted or
replaced with a stable placeholder; update the SentryEventScrubber logic to drop
or replace event.serverName and user identity fields (event.user?.username and
event.user?.name) rather than calling scrubber.scrub(optional:), e.g. set them
to nil or a constant placeholder inside the same branch where exceptions and
other fields are scrubbed (refer to event.serverName, event.user, and
scrubber.scrub(optional:) to locate the code), and update the expectations in
cmuxTests/SentryEventScrubberTests.swift to reflect the new redaction behavior.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 49821527-4afe-48bd-8095-4a5c9f3dae32

📥 Commits

Reviewing files that changed from the base of the PR and between 8d0ed95 and d960ea1.

📒 Files selected for processing (8)
  • CLI/cmux.swift
  • Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift
  • Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift
  • Packages/CmuxFoundation/Tests/CmuxFoundationTests/SentryScrubberTests.swift
  • Sources/AppDelegate.swift
  • Sources/SentryEventScrubber.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SentryEventScrubberTests.swift

Comment thread Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift Outdated
Comment on lines +51 to +54
public init(_ pattern: String, options: NSRegularExpression.Options = [.caseInsensitive]) {
// swiftlint:disable:next force_try
self.regex = try! NSRegularExpression(pattern: pattern, options: options)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant portion of the file with line numbers
FILE="Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift"
if [ -f "$FILE" ]; then
  echo "=== $FILE (lines 1-140) ==="
  nl -ba "$FILE" | sed -n '1,140p'
else
  echo "Missing: $FILE"
fi

# Find all initializations / uses
echo "=== rg: SentryRegexPattern init/usage ==="
rg -n "SentryRegexPattern\s*\(" -S . || true
rg -n "init\(_ pattern: String, options:" -S "$FILE" || true

Repository: manaflow-ai/cmux

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift"
nl -ba "$FILE" | sed -n '1,200p'

echo "---- usages ----"
rg -n "SentryRegexPattern\s*\(" -S . || true

Repository: manaflow-ai/cmux

Length of output: 103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift"
nl -ba "$FILE" | sed -n '1,200p' 

echo "---- usages ----"
rg -n "SentryRegexPattern\s*\(" -S . || true

Repository: manaflow-ai/cmux

Length of output: 103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift"

echo "=== file exists? ==="
ls -l "$FILE" || true

echo
echo "=== file contents (1-120) ==="
sed -n '1,120p' "$FILE" || true

echo
echo "=== try! usage in file ==="
rg -n "try!" "$FILE" || true

echo
echo "=== initializer signature in file ==="
rg -n "init\\(.*pattern: String" "$FILE" || true

echo
echo "=== usages across repo (max 200 lines) ==="
rg -n "SentryRegexPattern\\s*\\(" -S . | head -n 200 || true

Repository: manaflow-ai/cmux

Length of output: 5524


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift"

echo "=== file exists? ==="
ls -l "$FILE" || true

echo
echo "=== file contents (1-120) ==="
sed -n '1,120p' "$FILE" || true

echo
echo "=== try! usage in file ==="
rg -n "try!" "$FILE" || true

echo
echo "=== initializer signature in file ==="
rg -n "init\\(.*pattern: String" "$FILE" || true

echo
echo "=== usages across repo (max 200 lines) ==="
rg -n "SentryRegexPattern\\s*\\(" -S . | head -n 200 || true

Repository: manaflow-ai/cmux

Length of output: 5524


Avoid try! in SentryRegexPattern’s public initializer

public init(_ pattern: String, ...) force-unwraps NSRegularExpression, so invalid runtime input can crash the process. Change the initializer to throws or make the initializer non-public/internal if external callers aren’t meant to construct arbitrary patterns.

🤖 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 `@Packages/CmuxFoundation/Sources/CmuxFoundation/SentryRegexPattern.swift`
around lines 51 - 54, The public initializer SentryRegexPattern.init currently
uses try! to create self.regex which can crash on invalid patterns; change the
API to either make the initializer throwing (declare public init(_ pattern:
String, options: NSRegularExpression.Options = [.caseInsensitive]) throws and
use try to construct NSRegularExpression then assign to regex, propagating the
error to callers) or, if external construction isn’t intended, reduce the
initializer’s access level (make it internal or fileprivate) and handle the
NSRegularExpression construction with a failable/guard path instead of try!;
update any callers of SentryRegexPattern.init to either catch/propagate the
thrown error or to use the new internal factory so the code no longer
force-unwraps NSRegularExpression.

Source: Coding guidelines

Comment thread Sources/SentryEventScrubber.swift
Autoreview P1: the user context and request cookies are fields the scrubber
explicitly claims to protect, but pattern-scrubbing them leaks the common case.
A username/display name rarely looks like an email/path/secret, and a normal
session cookie (session=, sid=, auth=) has no secret-pattern name.

Drop user.userId/email/username/name/ipAddress/geo and request.cookies
wholesale instead of pattern-scrubbing. User.data and request URL/query/headers
still go through the scrubber.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7fbe98aa0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/SentryEventScrubber.swift Outdated
// …), so pattern-scrubbing the value cannot reliably catch every secret.
request.cookies = nil
if let headers = request.headers {
request.headers = headers.mapValues { scrubber.scrub($0) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact authorization headers using header names

When event.request.headers contains an Authorization header whose value is a non-Bearer scheme such as Basic dXNlcjpwYXNz, this code passes only the header value to the scrubber. The scrubber's authorization regex expects the literal Authorization: prefix, so these credentials remain in the Sentry payload even though this path is supposed to scrub request headers; use the header key to drop/redact sensitive headers before applying value-only patterns.

Useful? React with 👍 / 👎.

Comment thread Sources/SentryEventScrubber.swift
Comment thread cmuxTests/SentryEventScrubberTests.swift
Autoreview P2: two more egress paths the scrubber claimed to cover but missed.
capture(error:) copies NSError.userInfo (NSFilePathErrorKey, URLs, etc.) into
exception.mechanism.data, and event.debugMeta[].codeFile is the on-disk path to
the loaded binary, which carries /Users/<name>/ for dev/home-launched builds.

Scrub mechanism desc/helpLink/data (preserving mechanism.type for grouping) and
debug-image codeFile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@Sources/SentryEventScrubber.swift`:
- Around line 136-138: The request header scrubbing currently maps all headers
through scrubber.scrub but does not remove "Cookie" headers, so Cookie: ...
values can leak; update the logic in SentryEventScrubber (the block handling
request.headers and using mapValues { scrubber.scrub($0) }) to first remove any
header whose name equals "cookie" case-insensitively, then apply scrubber.scrub
to the remaining headers, and ensure request.headers is set to that
filtered+scrubbed dictionary. Add a unit test that supplies a Cookie header via
request.headers (e.g., "Cookie": "session=abc...") and asserts the Cookie header
has been dropped from the scrubbed request (header-based test to complement
existing request.cookies tests).
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 24e23526-a6ea-4232-8b0b-1fe53d05e77e

📥 Commits

Reviewing files that changed from the base of the PR and between d960ea1 and f7fbe98.

📒 Files selected for processing (2)
  • Sources/SentryEventScrubber.swift
  • cmuxTests/SentryEventScrubberTests.swift

Comment thread Sources/SentryEventScrubber.swift
Autoreview P1: the recursive dictionary scrubber only applied value regexes, so
a secret keyed by a sensitive name (context["auth"]["token"], extra, breadcrumb
data, or a Cookie/Authorization header) passed through when its raw value did
not match a standalone secret pattern.

Treat sensitive keys (token/password/secret/api_key/authorization/cookie/…,
case- and separator-insensitive) as a trust boundary: redact their string
values wholesale before recursing. Route request headers through the key-aware
dictionary scrubber too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

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

⚠️ Outside diff range comments (1)
Sources/SentryEventScrubber.swift (1)

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

serverName should be nil'd, not pattern-scrubbed.

The previous review flagged that serverName (like user.username/user.name) is an inherently identifying value that rarely matches secret/email/path patterns. A hostname like "lawrence-macbook.local" passes through unredacted. The user fields at lines 173-178 were fixed, but serverName was not.

🛡️ Suggested fix
-        event.serverName = scrubber.scrub(optional: event.serverName)
+        event.serverName = nil
🤖 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 `@Sources/SentryEventScrubber.swift` at line 44, Replace pattern-scrubbing of
serverName with niling: in SentryEventScrubber (where event.serverName is
currently set via scrubber.scrub(optional: event.serverName)) change the
assignment to set event.serverName = nil so hostnames like
"lawrence-macbook.local" are removed entirely rather than passed through; mirror
the same approach used for user.username/user.name scrub niling to keep behavior
consistent.
🤖 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.

Outside diff comments:
In `@Sources/SentryEventScrubber.swift`:
- Line 44: Replace pattern-scrubbing of serverName with niling: in
SentryEventScrubber (where event.serverName is currently set via
scrubber.scrub(optional: event.serverName)) change the assignment to set
event.serverName = nil so hostnames like "lawrence-macbook.local" are removed
entirely rather than passed through; mirror the same approach used for
user.username/user.name scrub niling to keep behavior consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 133dbf27-5325-43a0-b74b-d62bb2ed7ba0

📥 Commits

Reviewing files that changed from the base of the PR and between f7fbe98 and 74d9371.

📒 Files selected for processing (2)
  • Sources/SentryEventScrubber.swift
  • cmuxTests/SentryEventScrubberTests.swift

Comment thread Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift
Comment thread Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift Outdated
…llowlist

Autoreview P1 x2, fixed by inverting the default instead of chasing fields:
- Sensitive dictionary keys now drop their value wholesale regardless of shape
  (string, array, or nested dict), not just strings.
- Stack frames now scrub contextLine / preContext / postContext / vars (source
  snippets and local variable values), plus thread.name and event.logger.

The contract is now "scrub every free-text field; preserve only an explicit
grouping allowlist" (exception/mechanism type, fingerprint, frame
function/module/lineNumber/addresses, level, environment, release, dist,
modules), documented on the type. event.error is not serialized by the SDK
(converted to exceptions/mechanism.data first), so it needs no handling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread Sources/SentryEventScrubber.swift Outdated
Comment thread Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift Outdated
cmux-policy P2 (Aziz file-organization): one major type per file. Extract the
nested capture-aware match type out of SentryRegexPattern into SentryRegexMatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
Sources/SentryEventScrubber.swift (1)

85-87: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

event.tags bypasses sensitive-key redaction.

This path only scrubs tag values by content. A tag such as ["token": "session123"] or ["authorization": "abc"] survives if the value itself does not match a secret regex, even though SentryScrubber.scrub(dictionary:) already treats those keys as the redaction boundary. Route tags through the same key-aware dictionary scrubber used for extra, context, and headers, and add a matching regression case.

Suggested fix
         if let tags = event.tags {
-            event.tags = tags.mapValues { scrubber.scrub($0) }
+            event.tags = scrubber.scrub(dictionary: tags)
+                .compactMapValues { $0 as? String }
         }
🤖 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 `@Sources/SentryEventScrubber.swift` around lines 85 - 87, The tags branch in
SentryEventScrubber.swift currently only maps values with scrubber.scrub($0)
which ignores sensitive tag keys; change it to route event.tags through the same
key-aware dictionary scrubber (call SentryScrubber.scrub(dictionary:) or the
instance method scrub(dictionary:) on scrubber) so keys like "token" or
"authorization" are redacted, i.e., replace the tags.mapValues usage with a call
that scrubber.scrub(dictionary: event.tags) (or equivalent instance method) and
update/ add a regression test that submits an event with sensitive tag keys
(e.g., ["token":"session123"]) and asserts the tag value is redacted.
♻️ Duplicate comments (1)
Sources/SentryEventScrubber.swift (1)

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

Drop event.serverName wholesale.

serverName is identifying even when it contains no path, email, or token pattern, so hostnames like a user-named Mac or internal node name still leave the device through beforeSend. Pattern-scrubbing is not enough here; treat it like the other identity fields and clear it.

Suggested fix
-        event.serverName = scrubber.scrub(optional: event.serverName)
+        event.serverName = nil
🤖 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 `@Sources/SentryEventScrubber.swift` at line 53, The Sentry event's serverName
is still being scrubbed instead of removed, so hostnames can leak; update the
SentryEventScrubber logic to drop serverName entirely by setting
event.serverName = nil (instead of calling scrubber.scrub(optional:
event.serverName)) so it is cleared like other identity fields; locate the
assignment to event.serverName in Sources/SentryEventScrubber.swift and replace
the scrub call with a nil assignment ensuring no residual pattern-scrubbing
remains.
🤖 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 `@Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift`:
- Around line 150-156: The scrub(dictionary:) path uses Self.isSensitiveKey(key)
which currently relies on substring contains(marker) and thus over-redacts keys
like "author" or "tokenizer"; update isSensitiveKey to match against key
components and exact alias forms only (e.g., split camelCase/snake/kebab and
compare whole components and canonical exact names such as "password", "token",
"api_key", "auth-token", etc.) instead of raw substring containment, and ensure
scrub(dictionary:) and the same logic used in scrub(value:) call the revised
isSensitiveKey; add a regression unit test verifying a non-secret key like
"author" (and variations) is not redacted while actual secret keys are redacted.

---

Outside diff comments:
In `@Sources/SentryEventScrubber.swift`:
- Around line 85-87: The tags branch in SentryEventScrubber.swift currently only
maps values with scrubber.scrub($0) which ignores sensitive tag keys; change it
to route event.tags through the same key-aware dictionary scrubber (call
SentryScrubber.scrub(dictionary:) or the instance method scrub(dictionary:) on
scrubber) so keys like "token" or "authorization" are redacted, i.e., replace
the tags.mapValues usage with a call that scrubber.scrub(dictionary: event.tags)
(or equivalent instance method) and update/ add a regression test that submits
an event with sensitive tag keys (e.g., ["token":"session123"]) and asserts the
tag value is redacted.

---

Duplicate comments:
In `@Sources/SentryEventScrubber.swift`:
- Line 53: The Sentry event's serverName is still being scrubbed instead of
removed, so hostnames can leak; update the SentryEventScrubber logic to drop
serverName entirely by setting event.serverName = nil (instead of calling
scrubber.scrub(optional: event.serverName)) so it is cleared like other identity
fields; locate the assignment to event.serverName in
Sources/SentryEventScrubber.swift and replace the scrub call with a nil
assignment ensuring no residual pattern-scrubbing remains.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: d198aa7c-bced-4b89-91e2-88324d974737

📥 Commits

Reviewing files that changed from the base of the PR and between 5860017 and ecade1e.

📒 Files selected for processing (4)
  • Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift
  • Packages/CmuxFoundation/Tests/CmuxFoundationTests/SentryScrubberTests.swift
  • Sources/SentryEventScrubber.swift
  • cmuxTests/SentryEventScrubberTests.swift

Comment thread Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift
Autoreview P2 x2:
- event.tags went through content-only scrubbing, so a tag keyed
  `access_token` with an opaque value leaked. Route tags through the key-aware
  dictionary scrubber.
- The free-text assignment regex only matched bare markers (`secret=`,
  `api_key=`); env/config names embedding the marker (AWS_SECRET_ACCESS_KEY=,
  SECRET_ACCESS_KEY=, MY_API_KEY=) slipped through in messages / breadcrumbs /
  stack context. Allow identifier characters around the marker word.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4db5223d54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +131 to +136
case let dictionary as [String: Any]:
return scrub(dictionary: dictionary)
case let array as [Any]:
return array.map { scrub(value: $0) }
default:
return value

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scrub URL values before passing dictionaries through

When Sentry payload dictionaries contain URL/NSURL objects rather than strings (for example NSError.userInfo entries such as a failing file/HTTP URL copied into mechanism.data), this switch falls through to default and leaves the object untouched, so Sentry can still serialize an absolute URL containing /Users/<name>/... or token query parameters. Please normalize URL values to their string form and run them through the same scrubber before returning.

Useful? React with 👍 / 👎.

Autoreview P1: the macOS app samples transactions (tracesSampleRate = 0.1) and
Sentry processes child spans through beforeSendSpan, not beforeSend. Auto
network/file-I/O spans serialize URLs (http.query, url) and file paths in
spanDescription and data, which bypassed the event/breadcrumb scrubber.

Add a span scrubber (description + key-aware data/tags) and wire
options.beforeSendSpan in the macOS init. The CLI sets tracesSampleRate = 0, so
it emits no spans and needs no span hook. Operation/origin (span kind) are
preserved.

A dedicated span unit test is not practical: the concrete Span is only
constructable via a live SDK transaction, which is global SDK state. The span
path routes through scrub(dictionary:)/scrub(optional:), both covered by the
SentryScrubber package tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e031784. Configure here.

Comment thread Packages/CmuxFoundation/Sources/CmuxFoundation/SentryScrubber.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e031784b19

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +54 to +58
static let secretPatterns: [SentryRegexPattern] = [
// Bearer <token>
SentryRegexPattern(#"(Bearer\s+)[A-Za-z0-9\-._~+/]+=*"#),
// Authorization: <scheme> <token> (Basic / Digest / token / etc.)
SentryRegexPattern(#"(Authorization:\s*\w+\s+)\S+"#),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact credentials embedded in URL user-info

When a Sentry field routed through scrub(_:) contains a URL with RFC 3986 user-info, such as https://alice:hunter2@example.com/path, none of these secret patterns match because there is no password= key or Authorization: prefix. This leaves credentials in event.request.url, breadcrumb data, or context URL strings even though this scrubber is intended to sanitize URLs before send; add URL-aware redaction for the user[:password]@host component.

Useful? React with 👍 / 👎.

…y set

Autoreview P1: the string assignment regex used a narrower marker set than the
key-aware dictionary path, so credentials like auth=, session_id=, cookie=, or
private_key= in raw query strings / http.query span fields slipped through while
the same names were redacted as dictionary keys.

Expand the assignment alternation to match the dictionary sensitive markers
(auth, cookie, credential(s), session_id, private_key, bearer, …) so a
credential is redacted whether it arrives as a dict entry or as raw text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…race path

Autoreview P1 (verified against sentry-cocoa 9.3 source): SentryTransaction
serialize() merges the root trace's data/tags/description into the payload AFTER
beforeSend runs, and beforeSendSpan only iterates child spans, not the root
tracer. SentryTransaction/SentryTracer live in the SDK's internal include/
headers (not the public umbrella), so the root trace is not reachable to scrub
through the public Sentry API.

cmux does not consume these performance traces (no manual transactions or
span.setData), so disable transactions on macOS (tracesSampleRate = 0). This
removes the un-scrubbable egress path while keeping crash, error, and app-hang
reporting, which are independent of the trace sample rate. The beforeSendSpan
hook stays as belt-and-suspenders if tracing is ever re-enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e9cbb5570

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/SentryEventScrubber.swift Outdated
// `context` carries the per-key dictionaries set via
// `scope.setContext(value:key:)`, where cmux puts cwd / path / URL
// data; scrub every nested value.
event.context = context.mapValues { scrubber.scrub(dictionary: $0) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact contexts named by sensitive keys

When a caller uses the Sentry context key itself to describe a secret-bearing payload, e.g. scope.setContext(value: ["value": "dXNlcjpwYXNz"], key: "auth") or "credentials", this path ignores that top-level key and only scrubs the nested dictionary by its inner keys/content. Opaque credentials under generic inner names therefore still leave the device even though the scrubber already treats auth/credential as sensitive keys elsewhere; apply the same key-aware boundary to the context key before recursing.

Useful? React with 👍 / 👎.

Autoreview P2: scrub(value:) passed non-string/dict/array values through
unchanged, but Sentry serializes unsupported Foundation objects (notably
URL/NSURL, which carry a file path) to their description after beforeSend. A
URL(fileURLWithPath: "/Users/alice/secret.txt") in context/extra/breadcrumb
data / frame vars / span data would emit the unscrubbed path.

Preserve safe scalars (NSNumber/Bool/Int/Double, Date, Data, NSNull); convert
URL/NSURL and any other object to its string form and scrub that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62b4370ec1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

private func redactPaths(in text: String) -> String {
var result = text
if !homeDirectory.isEmpty, homeDirectory != "/" {
result = result.replacingOccurrences(of: homeDirectory, with: Self.redactedHomePath(for: homeDirectory))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect path boundaries when replacing the home directory

When the runtime home is a prefix of another username (for example homeDirectory == "/Users/ann" and a stack frame or NSError path contains /Users/anna/project), this unbounded string replacement rewrites it to /Users/<redacted>a/project before the generic /Users/<name>/ rule can run, leaving part of the other username in the outgoing Sentry payload. Match only the exact home path component (or let the generic user-home regex handle other /Users/.../ paths) so similarly prefixed usernames are fully redacted.

Useful? React with 👍 / 👎.

lawrencecchen and others added 3 commits June 7, 2026 22:27
The free-text + dictionary sensitive-key sets only knew session_id, so
request query strings like session=… or sid=… (common session credentials)
left the device unscrubbed. Add 'session' to the embeddable marker set + a
word-boundary \bsid pattern (so it never matches inside/aside), and an exact
'sid' dictionary-key marker (substring would over-redact innocuous keys).
+2 regression tests; full CmuxFoundation scrubber suite green (32 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sentry has no JSON binary type and serializes NSData to its hex description
AFTER beforeSend, so a Data value (e.g. Data("token=...")) under a
non-sensitive key leaked as hex. Stop treating Data as a safe scalar; drop it
wholesale to <redacted-data>. +1 regression test; suite green (33 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w P1+P2)

- URL user:pass@host authority (e.g. http://alice:secret@localhost) matched no
  rule; add a userinfo redactor that keeps scheme+host, run FIRST so it beats
  the email rule (which would match pass@host.tld and leak the username).
- Home-path rule required a trailing slash, so /Users/buildbot and
  file:///Users/alice leaked the username; drop the required slash (username
  component stops at the next delimiter/end).
+4 regression tests; suite green (35 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lawrencecchen and others added 5 commits June 7, 2026 23:01
…common values)

Move the hand-authored key markers and value regexes out of SentryScrubber
into ScrubberDenylists, ported from maintained upstream sources with the
exact commit SHA + file:line pinned per block so drift is auditable:

- Dictionary-key markers from sentry-python DEFAULT_DENYLIST +
  DEFAULT_PII_DENYLIST (@ 9e54e149a0, sentry_sdk/scrubber.py:15-60),
  reconciled with cmux's existing substring markers. Adds the session/
  CSRF/IP aliases the hand-rolled list missed (connect.sid, phpsessid,
  symfony, mysql_pwd, _csrf/_xsrf, x_forwarded_for/x_real_ip/ip_address/
  remote_addr) plus relay SENSITIVE_COOKIES aliases (sentrysid, _vercel_jwt,
  fasthttpsessionid, ...). Short aliases stored pre-normalized so they fire.
- Value regexes from relay @common (@ 99c91d9284, relay-pii/src/regexes.rs):
  adds @pemkey (PEM private/public key blocks), @creditcard, @iban, @usssn,
  ported with no capture group so cmux's keep-group-1-prefix loop redacts the
  whole match (relay's group-1 = redact convention is inverted from cmux's).
  Keeps cmux's existing bearer/key=value/provider-key/JWT/AWS and the
  existing urlUserInfo (@urlauth) rule; skips relay @ip/@uuid to avoid
  redacting version strings and the workspace/surface UUIDs cmux logs.

SentryScrubber's public API and all behavior are unchanged; the 35 existing
SentryScrubberTests pass untouched. Adds table-driven ScrubberDenylistsTests
(@test(arguments:)) asserting the ported keys/patterns redact representative
upstream-corpus secrets and that UUIDs/build numbers are not over-redacted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed @ (autoreview P1)

The URL credential class excluded @, so redis://user:p@ss@host/db redacted
only user:p@ and leaked ss@host (the host has no dotted domain, so the email
rule doesn't catch it either). Drop @ from the userinfo exclusion class so the
match consumes greedily through the last @ before a path/query/fragment/
whitespace terminator; the terminators still bound each match to one URL's
authority, so a later URL's host isn't swallowed. Adds regression coverage for
@-in-password, multi-credential strings, and the no-swallow case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(autoreview P1)

The free-text key=value rule stopped the value at the first whitespace, quote,
&, comma, or }, so a quoted JSON secret containing one of those characters
leaked its tail: {"password":"abc&def"} -> {"password":"<redacted-secret>&def"}.
Raw event messages, breadcrumbs, stack context lines, and NSError text arrive
as such strings and never hit the wholesale dict-key layer.

The terminator is quote-context-dependent: & must end an UNQUOTED query value
(?token=X&page=2 keeps &page=2) but not a QUOTED JSON value. Split the rule
(and the sid alias) into a quoted-value branch that captures the opening quote
and consumes through the closing quote ([^"']*), and an unquoted branch that
keeps the delimiter-bounded value. Quoted rules run first. Does not model
escaped quotes inside a quoted value (a regex JSON parser); the structured
dict-key layer plus the wholesale Data/user/cookie drops are the real boundary
and this free-text pass is best-effort defense-in-depth. Adds regression cases
for both branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lace (autoreview P1+P2)

P1: query strings drift from the key denylist. The free-text key=value regex
hand-lists a subset of markers, so query params whose key is in the denylist's
EXACT set (csrf, _csrf, _vercel_jwt, su, sentrysid, phpsessid, sid, ...) leaked.
Query strings are structured, so redact them structurally: new
SentryScrubber.scrubQueryString(_:) splits on &/; , parses each key=value, and
replaces the value with redactedSecret when isSensitiveKey(key) (URL-decoding the
key for the check). isSensitiveKey is now the single source of truth for query
strings. Glue routes request.queryString and request.url's query component
through it; the free-text scrub(_:) is documented as irreducibly best-effort.

P2: redactPaths did an unbounded substring replace of homeDirectory, so
homeDirectory=/Users/al over /Users/alice/x leaked the "ice" suffix. Precompute a
component-bounded SentryRegexPattern (escaped home + (?=[/\s"']|$) lookahead) in
init and use it instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…utoreview P2)

event.context is a [contextName: [key: value]] map. The glue mapped each inner
dict through scrub(dictionary:) but never checked the OUTER context name, so a
scope.setContext(value:["raw":"plainsecret"], key:"credentials") leaked when the
inner value matched no standalone secret pattern. tags/extra already get
key-aware scrubbing at their outer boundary; context now matches.

New SentryScrubber.scrub(context:) applies isSensitiveKey to the outer name
(redacting the whole context wholesale) and recurses through scrub(dictionary:)
otherwise. Glue routes event.context through it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d96e1a224

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
let key = String(segment[segment.startIndex..<equalsIndex])
let decodedKey = key.removingPercentEncoding ?? key
guard Self.isSensitiveKey(decodedKey) else { return segment }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scrub values for non-sensitive query keys

When event.request.queryString or a URL routed through scrubURL contains PII under an innocuous parameter name, this early return preserves the entire key=value segment without running the value through the normal string scrubber. For example file=/Users/alice/db.sqlite or next=https://host/p?token=opaque under a non-denylisted key leaves the user path or nested token in the Sentry payload; only keys on the denylist get redacted. Preserve non-sensitive keys, but still scrub their values by content before returning the segment unchanged.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4fa9ba7a6e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +200 to +201
case let string as String:
return scrub(string)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route URL strings through query scrubbing

When structured Sentry payloads carry a URL as a string under a non-sensitive key such as breadcrumb data["url"] or mechanism.data["url"], this dispatch sends it only through the free-text regex scrubber. Exact denylist query aliases like _csrf, sid, su, or _vercel_jwt are intentionally handled by scrubQueryString(_:) and are not covered by the free-text assignment patterns, so https://host/callback?_csrf=opaque remains unsanitized outside event.request.url; detect URL-like strings (or URL-valued keys) and route their query component through the structured query scrubber before returning.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen merged commit 6a16cd0 into main Jun 8, 2026
22 of 23 checks passed
@lawrencecchen
lawrencecchen deleted the feat-sentry-scrub branch June 8, 2026 09:22

This branch was successfully deployed

1 active deployment
Preview – cmux — 4fa9ba7a Deployed Jun 8, 2026 by vercel[bot]
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.

1 participant