feat(privacy): MONITOR_PRIVACY_REDACT — opt-in ingest-time secret redaction#221
feat(privacy): MONITOR_PRIVACY_REDACT — opt-in ingest-time secret redaction#221Mukller wants to merge 4 commits into
Conversation
Closes hoangsonww#182, contributes to hoangsonww#197. MONITOR_IGNORE_CWD (server/routes/hooks.js): - Parse comma-separated cwd patterns from env at startup into compiled matcher functions — zero runtime overhead per hook event beyond a single .some() call. - Three pattern forms supported, no external dependencies: /exact/path — strict equality after slash-normalisation /prefix/* — direct children only (no deeper nesting) /prefix/** — /prefix and all descendants - Early-return in router.post('/event') before processEvent(): ignored sessions are never written to the DB, never broadcast, and never appear in the UI, analytics, or exports. - Returns 200 { ok: true, ignored: true } so Claude Code does not retry — the event is intentionally discarded, not an error. - .env.example: document the variable with examples and caveats (pre-existing sessions are unaffected; use DELETE /api/sessions/:id). README-RU.md: - Full Russian translation of the project README (~376 lines). - Covers: overview, features, quick-start, hooks setup, config (including MONITOR_IGNORE_CWD section), API table, event types, deployment, project structure, and troubleshooting. - Language switcher footer linking all existing translations. - README.md: added README-RU.md to the localized-docs tip.
The backslash replacement used /\/g which is a broken regex literal (matches the string '/g, "' instead of a backslash). Changed to /\/g, the correct form for matching a literal backslash globally. Also tightened the isCwdIgnored guard from !cwd to typeof cwd !== "string" || !cwd so non-string payloads (null, objects) short-circuit safely before the string methods are called.
Move the MONITOR_IGNORE_CWD logic from an inline IIFE in hooks.js into a
dedicated server/lib/cwd-filter.js module so it can be tested in isolation
without standing up the full Express server.
Changes:
- server/lib/cwd-filter.js (new)
• buildPatterns(raw): compiles a comma-separated pattern string into an
array of matcher functions; exported so tests call it directly
• isCwdIgnored(cwd): thin wrapper over the module-level PATTERNS array
that reads from process.env.MONITOR_IGNORE_CWD at startup
• Supports exact, /*, and /** forms; normalises Windows backslashes
- server/routes/hooks.js
• Remove the inline IGNORE_CWD_PATTERNS block and isCwdIgnored function
• require('../lib/cwd-filter') instead — behaviour is unchanged
- server/__tests__/cwd-filter.test.js (new, 29 tests)
• empty / missing patterns → fast-path
• exact-match: match, child, sibling, parent, trailing-slash cases
• /* direct-children: child, grandchild, prefix-itself, sibling-prefix
• /** recursive: prefix, child, deep descendant, sibling-prefix, other
• multiple patterns: all three forms; whitespace around commas
• non-string cwd values: null, number, object, empty string
• Windows backslash normalisation in both cwd and pattern
…oangsonww#148) Add opt-in server-side payload redaction that scans hook event data for secret-like values and replaces them with [REDACTED] before the event is written to SQLite or broadcast over WebSocket. New files: server/lib/privacy-filter.js – core module (redactPayload, isRedactionEnabled, PATTERNS) server/__tests__/privacy-filter.test.js – 28 unit tests, all passing Changed files: server/routes/hooks.js – require + apply redactPayload when MONITOR_PRIVACY_REDACT=true .env.example – document MONITOR_PRIVACY_REDACT variable Built-in detectors (no external deps): • OpenAI / Anthropic / GitHub / Slack / Google SDK key prefixes • AWS AKIA/ASIA/AROA access key IDs • PEM private key blocks (RSA, EC, OPENSSH) • Bearer tokens in header-like strings • URL credentials (user:pass@host) • Generic api_key/secret_key/access_token assignments Structural routing fields (session_id, hook_event_id, cwd) are always preserved. Redaction errors cannot block ingestion (fail-safe via isRedactionEnabled() guard). Closes hoangsonww#148 (partial – backend module + 28 tests; no Settings UI)
There was a problem hiding this comment.
Code Review
This pull request introduces Russian documentation, an ingest-time working directory ignore filter via MONITOR_IGNORE_CWD, and an opt-in privacy redaction filter via MONITOR_PRIVACY_REDACT to mask secrets in hook event payloads. Feedback suggests preserving the transcript_path field from redaction to avoid breaking subsequent server operations, and optimizing the secret-matching logic by trimming string values once before iterating through patterns.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const out = {}; | ||
| for (const [key, val] of Object.entries(obj)) { | ||
| // Never redact structural identity / routing keys | ||
| if (key === "session_id" || key === "hook_event_id" || key === "cwd") { |
There was a problem hiding this comment.
The transcript_path property is a critical structural field used by the server to locate and parse session transcripts (e.g., for token extraction, compaction tracking, and watchdog checks). If the file path happens to contain a substring that matches any of the generic secret patterns (such as a directory named api_key or secret_key), the entire transcript_path will be redacted to [REDACTED]. This will cause subsequent transcript-reading operations to fail.\n\nAdditionally, skipping secret matching for transcript_path avoids unnecessary regex evaluations on file path strings.\n\nPlease add transcript_path to the list of preserved structural keys.
| if (key === "session_id" || key === "hook_event_id" || key === "cwd") { | |
| if (key === "session_id" || key === "hook_event_id" || key === "cwd" || key === "transcript_path") { |
| function matchSecret(value) { | ||
| if (typeof value !== "string" || value.length < 8) return null; | ||
| for (const { name, pattern, scan } of PATTERNS) { | ||
| if (scan ? pattern.test(value) : pattern.test(value.trim())) return name; | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
In matchSecret, value.trim() is called inside the loop for every pattern where scan is false. Since there are multiple such patterns (e.g., sdk-key-prefix and aws-key), this results in redundant string trimming and memory allocations on every check.\n\nTrimming the string once before the loop improves performance and reduces garbage collection overhead.
| function matchSecret(value) { | |
| if (typeof value !== "string" || value.length < 8) return null; | |
| for (const { name, pattern, scan } of PATTERNS) { | |
| if (scan ? pattern.test(value) : pattern.test(value.trim())) return name; | |
| } | |
| return null; | |
| } | |
| function matchSecret(value) { | |
| if (typeof value !== "string" || value.length < 8) return null; | |
| const trimmed = value.trim(); | |
| for (const { name, pattern, scan } of PATTERNS) { | |
| if (scan ? pattern.test(value) : pattern.test(trimmed)) return name; | |
| } | |
| return null; | |
| } |
|
I have read the CLA Document and I hereby sign the CLA |
Summary
Implements the server-side backend of the privacy controls requested in #148.
Adds a new opt-in feature that scans hook event payloads for secret-like values and replaces them with
[REDACTED]before the event is written to SQLite or broadcast over WebSocket.What's included
server/lib/privacy-filter.jsredactPayload(),isRedactionEnabled(),PATTERNSserver/__tests__/privacy-filter.test.jsserver/routes/hooks.jsprocessEvent().env.exampleMONITOR_PRIVACY_REDACTvariableActivation
# .env MONITOR_PRIVACY_REDACT=trueOff by default — zero impact to existing deployments.
Built-in detectors (no external dependencies)
sk-*,sk-ant-*,ghp_*,github_pat_*,xoxb-*,xoxp-*,AIza*,ya29.*AKIA*,ASIA*,AROA*(20-char format)user:pass@host)api_key=,secret_key=,access_token=,auth_token=Safety guarantees
session_id,hook_event_id,cwdare never touchedisRedactionEnabled()— a misconfigured env var cannot break ingestion/api/hooks/eventresponse includes"redactedCount": NwhenN > 0, enabling future observability without exposing original valuesNot included (out of scope for this PR)
Tests
Closes #148 (partial — backend module; UI to follow)