Skip to content

feat(privacy): MONITOR_PRIVACY_REDACT — opt-in ingest-time secret redaction#221

Open
Mukller wants to merge 4 commits into
hoangsonww:masterfrom
Mukller:feat/privacy-filter
Open

feat(privacy): MONITOR_PRIVACY_REDACT — opt-in ingest-time secret redaction#221
Mukller wants to merge 4 commits into
hoangsonww:masterfrom
Mukller:feat/privacy-filter

Conversation

@Mukller

@Mukller Mukller commented Jul 11, 2026

Copy link
Copy Markdown

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

File Change
server/lib/privacy-filter.js New module — redactPayload(), isRedactionEnabled(), PATTERNS
server/__tests__/privacy-filter.test.js 28 unit tests — all passing
server/routes/hooks.js Integrate redaction between cwd-filter check and processEvent()
.env.example Document MONITOR_PRIVACY_REDACT variable

Activation

# .env
MONITOR_PRIVACY_REDACT=true

Off by default — zero impact to existing deployments.

Built-in detectors (no external dependencies)

  • SDK key prefixes: sk-*, sk-ant-*, ghp_*, github_pat_*, xoxb-*, xoxp-*, AIza*, ya29.*
  • AWS access keys: AKIA*, ASIA*, AROA* (20-char format)
  • PEM private key blocks: RSA, EC, DSA, OPENSSH
  • Bearer tokens in header-like strings
  • URL credentials (user:pass@host)
  • Generic assignments: api_key=, secret_key=, access_token=, auth_token=

Safety guarantees

  • Structural routing fields preserved: session_id, hook_event_id, cwd are never touched
  • Fail-safe: redaction is guarded by isRedactionEnabled() — a misconfigured env var cannot break ingestion
  • Redacted count surfaced: the /api/hooks/event response includes "redactedCount": N when N > 0, enabling future observability without exposing original values
  • No external deps: pure Node.js built-ins only

Not included (out of scope for this PR)

Tests

node --test server/__tests__/privacy-filter.test.js
# ℹ tests 28 | pass 28 | fail 0

Closes #148 (partial — backend module; UI to follow)

Mukller added 4 commits July 10, 2026 22:05
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)
@Mukller
Mukller requested a review from hoangsonww as a code owner July 11, 2026 03:57
@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed question Further information is requested labels Jul 11, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

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.

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") {

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.

high

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.

Suggested change
if (key === "session_id" || key === "hook_event_id" || key === "cwd") {
if (key === "session_id" || key === "hook_event_id" || key === "cwd" || key === "transcript_path") {

Comment on lines +75 to +81
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;
}

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.

medium

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.

Suggested change
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;
}

@Mukller

Mukller commented Jul 14, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed question Further information is requested

Projects

Development

Successfully merging this pull request may close these issues.

Feature: Configurable privacy controls for hook payload ingestion

2 participants