From 5b56157e1f0b32340f8f9435c5d9e5f291fea4b0 Mon Sep 17 00:00:00 2001 From: Fahad Siddiqui <6371229+fahadsiddiqui@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:11:33 +0500 Subject: [PATCH] feat: add rule ID visibility, stable IDs, list_rules tool, and rules-picker CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the rule-discoverability and toggling work from issue #24. - Show each finding's rule ID in the hook's block message (e.g. "OpenAI API Key (openai-api-key): sk-p…3xyz"), plus a hint line pointing at disabledRules in .privacy-guard.json. - Make external rule IDs (data/regex_list_1.json) stable across data-file reordering: collisions are now disambiguated with a short hash of the entry's own content instead of a positional counter. - Add a list_rules MCP tool that returns id, title, severity, category, source, and disabled state for every rule, for use from inside a Claude Code chat session. - Add a companion CLI (bin: claude-code-privacy-guard, invoked as `npx claude-code-privacy-guard rules`) that starts a loopback-only local web UI listing every rule with checkboxes. Toggling a rule auto-saves (debounced) straight to disabledRules; the server stays alive across saves and only exits on Ctrl+C or 10 minutes of inactivity. The save endpoint is protected by a random per-run token as defense in depth. - Config resolution favors an existing project-level .privacy-guard.json, but falls back to a global ~/.privacy-guard.json by default when none exists anywhere in the directory tree, since Privacy Guard is meant to protect the person, not one repo. Both the console output and the web page label which scope (global vs. project-level override) is being edited. - Document all of the above in README.md, including the new "Managing Rules" section. --- README.md | 24 ++++- dist/cli/config-writer.d.ts | 16 ++++ dist/cli/config-writer.d.ts.map | 1 + dist/cli/config-writer.js | 37 ++++++++ dist/cli/config-writer.js.map | 1 + dist/cli/index.d.ts | 8 ++ dist/cli/index.d.ts.map | 1 + dist/cli/index.js | 21 +++++ dist/cli/index.js.map | 1 + dist/cli/page.d.ts | 14 +++ dist/cli/page.d.ts.map | 1 + dist/cli/page.js | 145 +++++++++++++++++++++++++++++ dist/cli/page.js.map | 1 + dist/cli/server.d.ts | 9 ++ dist/cli/server.d.ts.map | 1 + dist/cli/server.js | 111 +++++++++++++++++++++++ dist/cli/server.js.map | 1 + dist/index.js | 33 ++++++- dist/index.js.map | 2 +- dist/scanner/detectors.d.ts.map | 2 +- dist/scanner/detectors.js | 13 ++- dist/scanner/detectors.js.map | 2 +- package.json | 6 +- scripts/prompt-guard.js | 5 +- src/cli/config-writer.ts | 41 +++++++++ src/cli/index.ts | 24 +++++ src/cli/page.ts | 156 ++++++++++++++++++++++++++++++++ src/cli/server.ts | 125 +++++++++++++++++++++++++ src/index.ts | 36 +++++++- src/scanner/detectors.ts | 13 ++- 30 files changed, 828 insertions(+), 23 deletions(-) create mode 100644 dist/cli/config-writer.d.ts create mode 100644 dist/cli/config-writer.d.ts.map create mode 100644 dist/cli/config-writer.js create mode 100644 dist/cli/config-writer.js.map create mode 100644 dist/cli/index.d.ts create mode 100644 dist/cli/index.d.ts.map create mode 100755 dist/cli/index.js create mode 100644 dist/cli/index.js.map create mode 100644 dist/cli/page.d.ts create mode 100644 dist/cli/page.d.ts.map create mode 100644 dist/cli/page.js create mode 100644 dist/cli/page.js.map create mode 100644 dist/cli/server.d.ts create mode 100644 dist/cli/server.d.ts.map create mode 100644 dist/cli/server.js create mode 100644 dist/cli/server.js.map create mode 100644 src/cli/config-writer.ts create mode 100644 src/cli/index.ts create mode 100644 src/cli/page.ts create mode 100644 src/cli/server.ts diff --git a/README.md b/README.md index b98f746..e36dbca 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A privacy-first plugin for Claude Code that scans prompts for sensitive data and - ✅ **Works locally** - all scanning happens on your machine - ✅ **Zero configuration** - works out of the box - ✅ **Detailed reporting** - shows exactly what was detected +- ✅ **Toggle individual rules** - via `disabledRules`, a `list_rules` MCP tool, or `npx claude-code-privacy-guard rules` for a local web UI ## Installation @@ -74,13 +75,14 @@ My API key is sk-proj-abc123xyz and email is john@example.com 🛡️ Privacy Guard blocked this prompt Found 2 sensitive item(s): - - API_KEY: sk-proj-abc123xyz... - - EMAIL: john@example.com... + - OpenAI API Key (openai-api-key): sk-p…3xyz + - Email Address (email-address): john….com Risk Score: 100/100 Secrets: 1 | PII: 1 Please remove or anonymize sensitive data before proceeding. +To disable a rule, add its ID to "disabledRules" in .privacy-guard.json. ``` @@ -94,7 +96,7 @@ file. | Option | Type | Default | Status | | --- | --- | --- | --- | | `enabled` | `boolean` | `true` | ✅ Implemented. Set to `false` to disable the plugin entirely without uninstalling it. | -| `disabledRules` | `string[]` | `[]` | ✅ Implemented. Rule IDs to skip - see the built-in `id` fields in `src/scanner/detectors.ts` or the `name` fields in `data/regex_list_1.json` for external rules. | +| `disabledRules` | `string[]` | `[]` | ✅ Implemented. Rule IDs to skip - see [Managing Rules](#managing-rules) below for how to discover and toggle IDs. | | `externalRulesJsonPath` | `string` | `./data/regex_list_1.json` | ✅ Implemented. Path (relative to the config file's directory) to the external regex dataset. | | `externalRulesMode` | `"coding-only" \| "all"` | `"coding-only"` | ✅ Implemented. `"coding-only"` filters the external dataset down to rules whose name/description mentions a coding-secret keyword (key, token, secret, password, private key, etc.); `"all"` loads every external rule. | | `strictMode` | `boolean` | `false` | ⚠️ Accepted in config but not yet enforced by the scanner ([#6](https://github.com/datumbrain/claude-code-privacy-guard/issues/6)). | @@ -113,6 +115,22 @@ Example: } ``` +## Managing Rules + +Every detection rule - built-in and external - has a stable `id` (e.g. `email-address`, `openai-api-key`, `external-slack-api-token`). Blocked prompts now show each finding's ID directly, so you always know what to put in `disabledRules`. + +To see and toggle every rule at once, run: + +```bash +npx claude-code-privacy-guard rules +``` + +This starts a local-only web UI (bound to `127.0.0.1`, never exposed to your network) listing every rule with its ID, title, severity, and category. Toggling a rule saves automatically to `disabledRules`. + +It edits whichever `.privacy-guard.json` `ConfigLoader.findConfig` would resolve for the directory you run it from: an existing project-level config always wins, but if none exists anywhere up the directory tree it falls back to `~/.privacy-guard.json` - a systemwide default that applies to every project, since Privacy Guard is protecting you, not one repo. Run it from `~` (or any directory with no project-level config) to edit that global default; run it from inside a specific project to create or edit a project-level override instead. The page and console output both tell you which one you're editing. + +If you're working inside a Claude Code chat session instead, the MCP server also exposes a `list_rules` tool that returns the same id/title/severity/category/enabled data for each rule. + ## Development ```bash diff --git a/dist/cli/config-writer.d.ts b/dist/cli/config-writer.d.ts new file mode 100644 index 0000000..49f186f --- /dev/null +++ b/dist/cli/config-writer.d.ts @@ -0,0 +1,16 @@ +/** + * Reads and writes the disabledRules array in .privacy-guard.json without + * disturbing any other keys the user has set. + */ +/** + * Resolves which config file to edit. An existing project-level config + * (found by searching upward from cwd) always wins, since it's an explicit + * per-project override. Otherwise this falls back to the user's home + * directory rather than creating a new file in whatever project the CLI + * happened to be run from - Privacy Guard protects the person, not one repo, + * so a systemwide default is the sane default for a first save. + */ +export declare function resolveConfigPath(startDir?: string): string; +export declare function isGlobalConfigPath(configPath: string): boolean; +export declare function writeDisabledRules(configPath: string, disabledRules: string[]): void; +//# sourceMappingURL=config-writer.d.ts.map \ No newline at end of file diff --git a/dist/cli/config-writer.d.ts.map b/dist/cli/config-writer.d.ts.map new file mode 100644 index 0000000..dfcf6b5 --- /dev/null +++ b/dist/cli/config-writer.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"config-writer.d.ts","sourceRoot":"","sources":["../../src/cli/config-writer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAE1E;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI,CAcpF"} \ No newline at end of file diff --git a/dist/cli/config-writer.js b/dist/cli/config-writer.js new file mode 100644 index 0000000..e44522b --- /dev/null +++ b/dist/cli/config-writer.js @@ -0,0 +1,37 @@ +/** + * Reads and writes the disabledRules array in .privacy-guard.json without + * disturbing any other keys the user has set. + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { ConfigLoader } from '../config/loader.js'; +/** + * Resolves which config file to edit. An existing project-level config + * (found by searching upward from cwd) always wins, since it's an explicit + * per-project override. Otherwise this falls back to the user's home + * directory rather than creating a new file in whatever project the CLI + * happened to be run from - Privacy Guard protects the person, not one repo, + * so a systemwide default is the sane default for a first save. + */ +export function resolveConfigPath(startDir = process.cwd()) { + return ConfigLoader.findConfig(startDir) || path.join(os.homedir(), '.privacy-guard.json'); +} +export function isGlobalConfigPath(configPath) { + return path.dirname(configPath) === os.homedir(); +} +export function writeDisabledRules(configPath, disabledRules) { + let existing = {}; + if (fs.existsSync(configPath)) { + try { + existing = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + } + catch { + // Malformed existing config - fail loudly rather than silently clobbering it. + throw new Error(`Could not parse existing config at ${configPath}; fix or remove it before saving.`); + } + } + const updated = { ...existing, disabledRules: [...disabledRules].sort() }; + fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n', 'utf-8'); +} +//# sourceMappingURL=config-writer.js.map \ No newline at end of file diff --git a/dist/cli/config-writer.js.map b/dist/cli/config-writer.js.map new file mode 100644 index 0000000..f06fd9d --- /dev/null +++ b/dist/cli/config-writer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"config-writer.js","sourceRoot":"","sources":["../../src/cli/config-writer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAmB,OAAO,CAAC,GAAG,EAAE;IAChE,OAAO,YAAY,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,qBAAqB,CAAC,CAAC;AAC7F,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,UAAkB;IACnD,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,UAAkB,EAAE,aAAuB;IAC5E,IAAI,QAAQ,GAA4B,EAAE,CAAC;IAE3C,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,8EAA8E;YAC9E,MAAM,IAAI,KAAK,CAAC,sCAAsC,UAAU,mCAAmC,CAAC,CAAC;QACvG,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,EAAE,GAAG,QAAQ,EAAE,aAAa,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;IAC1E,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AACjF,CAAC"} \ No newline at end of file diff --git a/dist/cli/index.d.ts b/dist/cli/index.d.ts new file mode 100644 index 0000000..41f97a8 --- /dev/null +++ b/dist/cli/index.d.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +/** + * claude-code-privacy-guard CLI + * + * Usage: npx claude-code-privacy-guard rules + */ +export {}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/cli/index.d.ts.map b/dist/cli/index.d.ts.map new file mode 100644 index 0000000..84df1d9 --- /dev/null +++ b/dist/cli/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA;;;;GAIG"} \ No newline at end of file diff --git a/dist/cli/index.js b/dist/cli/index.js new file mode 100755 index 0000000..64c9fd6 --- /dev/null +++ b/dist/cli/index.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node +/** + * claude-code-privacy-guard CLI + * + * Usage: npx claude-code-privacy-guard rules + */ +import { startRulesServer } from './server.js'; +const [, , command] = process.argv; +switch (command) { + case 'rules': + case undefined: + startRulesServer().catch((error) => { + console.error('Failed to start rules editor:', error); + process.exit(1); + }); + break; + default: + console.error(`Unknown command: ${command}\n\nUsage: claude-code-privacy-guard rules`); + process.exit(1); +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/cli/index.js.map b/dist/cli/index.js.map new file mode 100644 index 0000000..8032ce4 --- /dev/null +++ b/dist/cli/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA;;;;GAIG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,CAAC,EAAE,AAAD,EAAG,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;AAEnC,QAAQ,OAAO,EAAE,CAAC;IAChB,KAAK,OAAO,CAAC;IACb,KAAK,SAAS;QACZ,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACjC,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,MAAM;IACR;QACE,OAAO,CAAC,KAAK,CAAC,oBAAoB,OAAO,4CAA4C,CAAC,CAAC;QACvF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC"} \ No newline at end of file diff --git a/dist/cli/page.d.ts b/dist/cli/page.d.ts new file mode 100644 index 0000000..cec2210 --- /dev/null +++ b/dist/cli/page.d.ts @@ -0,0 +1,14 @@ +/** + * Self-contained HTML page for the local rules-picker UI. No external + * assets/CDNs - everything is inlined so it works fully offline. + */ +export interface RuleRow { + id: string; + title: string; + severity: string; + category: string; + source: 'builtin' | 'external'; + disabled: boolean; +} +export declare function renderPage(rules: RuleRow[], token: string, configPath: string, isGlobal: boolean): string; +//# sourceMappingURL=page.d.ts.map \ No newline at end of file diff --git a/dist/cli/page.d.ts.map b/dist/cli/page.d.ts.map new file mode 100644 index 0000000..08205c3 --- /dev/null +++ b/dist/cli/page.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/cli/page.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAyIzG"} \ No newline at end of file diff --git a/dist/cli/page.js b/dist/cli/page.js new file mode 100644 index 0000000..3f55b73 --- /dev/null +++ b/dist/cli/page.js @@ -0,0 +1,145 @@ +/** + * Self-contained HTML page for the local rules-picker UI. No external + * assets/CDNs - everything is inlined so it works fully offline. + */ +export function renderPage(rules, token, configPath, isGlobal) { + const rulesJson = JSON.stringify(rules).replace(/ + + + +Privacy Guard - Rule Toggles + + + +

Privacy Guard - Rule Toggles

+
${escapeHtml(scopeLabel)}
Config: ${escapeHtml(configPath)} - uncheck a rule to disable it, saved automatically to disabledRules
+
+ + +
+
+ + + + +`; +} +function escapeHtml(value) { + return value.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); +} +//# sourceMappingURL=page.js.map \ No newline at end of file diff --git a/dist/cli/page.js.map b/dist/cli/page.js.map new file mode 100644 index 0000000..274b1aa --- /dev/null +++ b/dist/cli/page.js.map @@ -0,0 +1 @@ +{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/cli/page.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,UAAU,UAAU,CAAC,KAAgB,EAAE,KAAa,EAAE,UAAkB,EAAE,QAAiB;IAC/F,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACjE,MAAM,UAAU,GAAG,QAAQ;QACzB,CAAC,CAAC,oFAAoF;QACtF,CAAC,CAAC,wBAAwB,CAAC;IAE7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BA+BiB,UAAU,CAAC,UAAU,CAAC,qBAAqB,UAAU,CAAC,UAAU,CAAC;;;;;;YAM/E,KAAK,CAAC,MAAM;;;gBAGR,SAAS;gBACT,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAyF7B,CAAC;AACT,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAY,CAAA,CAAC,CAAC;AAClI,CAAC"} \ No newline at end of file diff --git a/dist/cli/server.d.ts b/dist/cli/server.d.ts new file mode 100644 index 0000000..aeb7f0f --- /dev/null +++ b/dist/cli/server.d.ts @@ -0,0 +1,9 @@ +/** + * Local-only HTTP server for the rules-picker UI. Binds to loopback only, + * requires a random per-run token on the save endpoint (defense in depth - + * the JSON content-type already blocks simple cross-origin POSTs), and + * shuts itself down after a period of inactivity (saving keeps it alive so + * you can toggle and save again without restarting). + */ +export declare function startRulesServer(): Promise; +//# sourceMappingURL=server.d.ts.map \ No newline at end of file diff --git a/dist/cli/server.d.ts.map b/dist/cli/server.d.ts.map new file mode 100644 index 0000000..191a8b3 --- /dev/null +++ b/dist/cli/server.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/cli/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CA8FtD"} \ No newline at end of file diff --git a/dist/cli/server.js b/dist/cli/server.js new file mode 100644 index 0000000..65dc60e --- /dev/null +++ b/dist/cli/server.js @@ -0,0 +1,111 @@ +/** + * Local-only HTTP server for the rules-picker UI. Binds to loopback only, + * requires a random per-run token on the save endpoint (defense in depth - + * the JSON content-type already blocks simple cross-origin POSTs), and + * shuts itself down after a period of inactivity (saving keeps it alive so + * you can toggle and save again without restarting). + */ +import * as http from 'http'; +import { randomBytes } from 'crypto'; +import { fileURLToPath } from 'url'; +import { BUILTIN_RULES, loadExternalRulesFromJson } from '../scanner/detectors.js'; +import { ConfigLoader } from '../config/loader.js'; +import { renderPage } from './page.js'; +import { resolveConfigPath, isGlobalConfigPath, writeDisabledRules } from './config-writer.js'; +const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; +export async function startRulesServer() { + const configPath = resolveConfigPath(); + const configLoader = new ConfigLoader(configPath); + const config = configLoader.getConfig(); + const defaultExternalRulesPath = fileURLToPath(new URL('../../data/regex_list_1.json', import.meta.url)); + const externalRulesPath = config.externalRulesJsonPath || defaultExternalRulesPath; + const externalRules = loadExternalRulesFromJson(externalRulesPath, { + codingOnly: config.externalRulesMode !== 'all', + }); + const disabledRules = new Set(config.disabledRules); + const allRules = [...BUILTIN_RULES, ...externalRules]; + const rules = allRules.map((rule) => ({ + id: rule.id, + title: rule.title, + severity: rule.severity, + category: rule.category, + source: rule.id.startsWith('external-') ? 'external' : 'builtin', + disabled: disabledRules.has(rule.id), + })); + const token = randomBytes(16).toString('hex'); + const server = http.createServer((req, res) => { + resetInactivityTimer(); + if (req.method === 'GET' && req.url === '/') { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(renderPage(rules, token, configPath, isGlobalConfigPath(configPath))); + return; + } + if (req.method === 'POST' && req.url === '/save') { + let body = ''; + req.on('data', (chunk) => { + body += chunk; + if (body.length > 1_000_000) + req.destroy(); + }); + req.on('end', () => { + try { + if (req.headers['content-type'] !== 'application/json') { + throw new Error('Expected application/json'); + } + const parsed = JSON.parse(body); + if (parsed.token !== token) { + res.writeHead(403, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: false, error: 'Invalid token' })); + return; + } + if (!Array.isArray(parsed.disabledRules) || !parsed.disabledRules.every((id) => typeof id === 'string')) { + throw new Error('disabledRules must be a string array'); + } + writeDisabledRules(configPath, parsed.disabledRules); + disabledRules.clear(); + for (const id of parsed.disabledRules) + disabledRules.add(id); + for (const rule of rules) + rule.disabled = disabledRules.has(rule.id); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + } + catch (error) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: false, error: error.message })); + } + }); + return; + } + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not found'); + }); + let inactivityTimer; + function resetInactivityTimer() { + clearTimeout(inactivityTimer); + inactivityTimer = setTimeout(() => server.close(), INACTIVITY_TIMEOUT_MS); + } + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + resetInactivityTimer(); + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : 0; + const url = `http://127.0.0.1:${port}/`; + console.log(`Privacy Guard rules editor running at ${url}`); + console.log(`Config file: ${configPath}${isGlobalConfigPath(configPath) ? ' (global - applies to every project unless it has its own .privacy-guard.json)' : ' (project-level override)'}`); + console.log(`Loaded ${rules.length} rules (${disabledRules.size} currently disabled).`); + console.log('Press Ctrl+C to stop (the server also exits after 10 minutes of inactivity).'); + await openInBrowser(url); + await new Promise((resolve) => server.on('close', resolve)); +} +async function openInBrowser(url) { + const { spawn } = await import('child_process'); + const platform = process.platform; + const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'; + try { + spawn(command, [url], { stdio: 'ignore', detached: true, shell: platform === 'win32' }).unref(); + } + catch { + console.log('Could not auto-open a browser - open the URL above manually.'); + } +} +//# sourceMappingURL=server.js.map \ No newline at end of file diff --git a/dist/cli/server.js.map b/dist/cli/server.js.map new file mode 100644 index 0000000..ef65032 --- /dev/null +++ b/dist/cli/server.js.map @@ -0,0 +1 @@ +{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/cli/server.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 08026c4..32791da 100644 --- a/dist/index.js +++ b/dist/index.js @@ -20,6 +20,8 @@ class PrivacyGuardServer { server; scanner; config; + allRules; + disabledRules; constructor() { this.server = new Server({ name: 'claude-code-privacy-guard', @@ -39,8 +41,9 @@ class PrivacyGuardServer { const externalRules = loadExternalRulesFromJson(externalRulesPath, { codingOnly: this.config.externalRulesMode !== 'all', }); - const disabledRules = new Set(this.config.disabledRules); - const activeRules = [...BUILTIN_RULES, ...externalRules].filter((rule) => !disabledRules.has(rule.id)); + this.allRules = [...BUILTIN_RULES, ...externalRules]; + this.disabledRules = new Set(this.config.disabledRules); + const activeRules = this.allRules.filter((rule) => !this.disabledRules.has(rule.id)); this.scanner = new PrivacyScanner(activeRules); this.setupHandlers(); // Error handling @@ -82,6 +85,14 @@ class PrivacyGuardServer { required: ['text'], }, }, + { + name: 'list_rules', + description: 'List every detection rule (built-in and external) with its ID, title, severity, category, and whether it is currently disabled via disabledRules', + inputSchema: { + type: 'object', + properties: {}, + }, + }, ], })); // Handle tool calls @@ -124,6 +135,24 @@ class PrivacyGuardServer { ], }; } + case 'list_rules': { + const rules = this.allRules.map((rule) => ({ + id: rule.id, + title: rule.title, + severity: rule.severity, + category: rule.category, + source: rule.id.startsWith('external-') ? 'external' : 'builtin', + disabled: this.disabledRules.has(rule.id), + })); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ rules, total: rules.length, disabledCount: this.disabledRules.size }, null, 2), + }, + ], + }; + } default: throw new Error(`Unknown tool: ${name}`); } diff --git a/dist/index.js.map b/dist/index.js.map index f36e0f3..ebf6f70 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/scanner/detectors.d.ts.map b/dist/scanner/detectors.d.ts.map index 78f9e0b..38ee924 100644 --- a/dist/scanner/detectors.d.ts.map +++ b/dist/scanner/detectors.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"detectors.d.ts","sourceRoot":"","sources":["../../src/scanner/detectors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAWrD,UAAU,mBAAmB;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,aAAa,EAsKxC,CAAC;AAgDF;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,mBAAwB,GAChC,aAAa,EAAE,CAsDjB"} \ No newline at end of file +{"version":3,"file":"detectors.d.ts","sourceRoot":"","sources":["../../src/scanner/detectors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAYrD,UAAU,mBAAmB;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,aAAa,EAsKxC,CAAC;AAgDF;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,mBAAwB,GAChC,aAAa,EAAE,CAwDjB"} \ No newline at end of file diff --git a/dist/scanner/detectors.js b/dist/scanner/detectors.js index fa127d9..d116a8e 100644 --- a/dist/scanner/detectors.js +++ b/dist/scanner/detectors.js @@ -2,6 +2,7 @@ * Pattern detectors for sensitive data */ import * as fs from 'fs'; +import { createHash } from 'crypto'; /** * Built-in detection rules * Start with high-value patterns that catch the most dangerous leaks @@ -243,11 +244,13 @@ export function loadExternalRulesFromJson(jsonPath, options = {}) { catch { continue; } - let id = `external-${slugify(entry.name)}`; - let suffix = 2; - while (usedIds.has(id)) { - id = `external-${slugify(entry.name)}-${suffix}`; - suffix += 1; + // Suffix with a short hash of the entry's own content (not its position) + // so IDs stay stable across data-file reordering, even on name collisions. + const baseId = `external-${slugify(entry.name)}`; + let id = baseId; + if (usedIds.has(id)) { + const contentHash = createHash('sha1').update(`${entry.name}:${entry.regex}`).digest('hex').slice(0, 6); + id = `${baseId}-${contentHash}`; } usedIds.add(id); rules.push({ diff --git a/dist/scanner/detectors.js.map b/dist/scanner/detectors.js.map index f0d7ab4..3d1f958 100644 --- a/dist/scanner/detectors.js.map +++ b/dist/scanner/detectors.js.map @@ -1 +1 @@ -{"version":3,"file":"detectors.js","sourceRoot":"","sources":["../../src/scanner/detectors.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"detectors.js","sourceRoot":"","sources":["../../src/scanner/detectors.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index 0669223..f1debdd 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,12 @@ "description": "Privacy guard plugin for Claude Code - prevents PII and secrets from leaking into AI prompts", "main": "dist/index.js", "bin": { - "claude-code-privacy-guard": "dist/index.js" + "claude-code-privacy-guard": "dist/cli/index.js" }, + "files": [ + "dist", + "data" + ], "scripts": { "build": "tsc", "dev": "tsc --watch", diff --git a/scripts/prompt-guard.js b/scripts/prompt-guard.js index 60a382f..74d40f1 100755 --- a/scripts/prompt-guard.js +++ b/scripts/prompt-guard.js @@ -61,7 +61,7 @@ function maskMatch(value) { if (result.findings.length > 0) { // Build detailed findings list const findingsList = result.findings.map(f => - ` - ${f.title}: ${maskMatch(f.match)}` + ` - ${f.title} (${f.ruleId}): ${maskMatch(f.match)}` ).join('\n'); // Return blocking decision as JSON @@ -71,7 +71,8 @@ if (result.findings.length > 0) { `Found ${result.findings.length} sensitive item(s):\n${findingsList}\n\n` + `Risk Score: ${result.riskScore}/100\n` + `Secrets: ${result.summary.secret || 0} | PII: ${result.summary.pii || 0}\n\n` + - `Please remove or anonymize sensitive data before proceeding.` + `Please remove or anonymize sensitive data before proceeding.\n` + + `To disable a rule, add its ID to "disabledRules" in .privacy-guard.json.` }; console.log(JSON.stringify(response, null, 2)); diff --git a/src/cli/config-writer.ts b/src/cli/config-writer.ts new file mode 100644 index 0000000..41fb149 --- /dev/null +++ b/src/cli/config-writer.ts @@ -0,0 +1,41 @@ +/** + * Reads and writes the disabledRules array in .privacy-guard.json without + * disturbing any other keys the user has set. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { ConfigLoader } from '../config/loader.js'; + +/** + * Resolves which config file to edit. An existing project-level config + * (found by searching upward from cwd) always wins, since it's an explicit + * per-project override. Otherwise this falls back to the user's home + * directory rather than creating a new file in whatever project the CLI + * happened to be run from - Privacy Guard protects the person, not one repo, + * so a systemwide default is the sane default for a first save. + */ +export function resolveConfigPath(startDir: string = process.cwd()): string { + return ConfigLoader.findConfig(startDir) || path.join(os.homedir(), '.privacy-guard.json'); +} + +export function isGlobalConfigPath(configPath: string): boolean { + return path.dirname(configPath) === os.homedir(); +} + +export function writeDisabledRules(configPath: string, disabledRules: string[]): void { + let existing: Record = {}; + + if (fs.existsSync(configPath)) { + try { + existing = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + } catch { + // Malformed existing config - fail loudly rather than silently clobbering it. + throw new Error(`Could not parse existing config at ${configPath}; fix or remove it before saving.`); + } + } + + const updated = { ...existing, disabledRules: [...disabledRules].sort() }; + fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n', 'utf-8'); +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..2408e04 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +/** + * claude-code-privacy-guard CLI + * + * Usage: npx claude-code-privacy-guard rules + */ + +import { startRulesServer } from './server.js'; + +const [, , command] = process.argv; + +switch (command) { + case 'rules': + case undefined: + startRulesServer().catch((error) => { + console.error('Failed to start rules editor:', error); + process.exit(1); + }); + break; + default: + console.error(`Unknown command: ${command}\n\nUsage: claude-code-privacy-guard rules`); + process.exit(1); +} diff --git a/src/cli/page.ts b/src/cli/page.ts new file mode 100644 index 0000000..def5872 --- /dev/null +++ b/src/cli/page.ts @@ -0,0 +1,156 @@ +/** + * Self-contained HTML page for the local rules-picker UI. No external + * assets/CDNs - everything is inlined so it works fully offline. + */ + +export interface RuleRow { + id: string; + title: string; + severity: string; + category: string; + source: 'builtin' | 'external'; + disabled: boolean; +} + +export function renderPage(rules: RuleRow[], token: string, configPath: string, isGlobal: boolean): string { + const rulesJson = JSON.stringify(rules).replace(/ + + + +Privacy Guard - Rule Toggles + + + +

Privacy Guard - Rule Toggles

+
${escapeHtml(scopeLabel)}
Config: ${escapeHtml(configPath)} - uncheck a rule to disable it, saved automatically to disabledRules
+
+ + +
+
+
${rules.length} rules total. Toggles save automatically - stop the server from the terminal (Ctrl+C) when you're done, or it exits after 10 minutes of inactivity.
+ + + +`; +} + +function escapeHtml(value: string): string { + return value.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] as string)); +} diff --git a/src/cli/server.ts b/src/cli/server.ts new file mode 100644 index 0000000..901f305 --- /dev/null +++ b/src/cli/server.ts @@ -0,0 +1,125 @@ +/** + * Local-only HTTP server for the rules-picker UI. Binds to loopback only, + * requires a random per-run token on the save endpoint (defense in depth - + * the JSON content-type already blocks simple cross-origin POSTs), and + * shuts itself down after a period of inactivity (saving keeps it alive so + * you can toggle and save again without restarting). + */ + +import * as http from 'http'; +import { randomBytes } from 'crypto'; +import { fileURLToPath } from 'url'; + +import { BUILTIN_RULES, loadExternalRulesFromJson } from '../scanner/detectors.js'; +import { ConfigLoader } from '../config/loader.js'; +import { renderPage, RuleRow } from './page.js'; +import { resolveConfigPath, isGlobalConfigPath, writeDisabledRules } from './config-writer.js'; + +const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; + +export async function startRulesServer(): Promise { + const configPath = resolveConfigPath(); + const configLoader = new ConfigLoader(configPath); + const config = configLoader.getConfig(); + + const defaultExternalRulesPath = fileURLToPath(new URL('../../data/regex_list_1.json', import.meta.url)); + const externalRulesPath = config.externalRulesJsonPath || defaultExternalRulesPath; + const externalRules = loadExternalRulesFromJson(externalRulesPath, { + codingOnly: config.externalRulesMode !== 'all', + }); + + const disabledRules = new Set(config.disabledRules); + const allRules = [...BUILTIN_RULES, ...externalRules]; + const rules: RuleRow[] = allRules.map((rule) => ({ + id: rule.id, + title: rule.title, + severity: rule.severity, + category: rule.category, + source: rule.id.startsWith('external-') ? 'external' : 'builtin', + disabled: disabledRules.has(rule.id), + })); + + const token = randomBytes(16).toString('hex'); + + const server = http.createServer((req, res) => { + resetInactivityTimer(); + + if (req.method === 'GET' && req.url === '/') { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(renderPage(rules, token, configPath, isGlobalConfigPath(configPath))); + return; + } + + if (req.method === 'POST' && req.url === '/save') { + let body = ''; + req.on('data', (chunk) => { + body += chunk; + if (body.length > 1_000_000) req.destroy(); + }); + req.on('end', () => { + try { + if (req.headers['content-type'] !== 'application/json') { + throw new Error('Expected application/json'); + } + const parsed = JSON.parse(body) as { token?: string; disabledRules?: unknown }; + if (parsed.token !== token) { + res.writeHead(403, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: false, error: 'Invalid token' })); + return; + } + if (!Array.isArray(parsed.disabledRules) || !parsed.disabledRules.every((id) => typeof id === 'string')) { + throw new Error('disabledRules must be a string array'); + } + + writeDisabledRules(configPath, parsed.disabledRules as string[]); + disabledRules.clear(); + for (const id of parsed.disabledRules as string[]) disabledRules.add(id); + for (const rule of rules) rule.disabled = disabledRules.has(rule.id); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + } catch (error) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: false, error: (error as Error).message })); + } + }); + return; + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not found'); + }); + + let inactivityTimer: NodeJS.Timeout; + function resetInactivityTimer() { + clearTimeout(inactivityTimer); + inactivityTimer = setTimeout(() => server.close(), INACTIVITY_TIMEOUT_MS); + } + + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + resetInactivityTimer(); + + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : 0; + const url = `http://127.0.0.1:${port}/`; + + console.log(`Privacy Guard rules editor running at ${url}`); + console.log(`Config file: ${configPath}${isGlobalConfigPath(configPath) ? ' (global - applies to every project unless it has its own .privacy-guard.json)' : ' (project-level override)'}`); + console.log(`Loaded ${rules.length} rules (${disabledRules.size} currently disabled).`); + console.log('Press Ctrl+C to stop (the server also exits after 10 minutes of inactivity).'); + + await openInBrowser(url); + + await new Promise((resolve) => server.on('close', resolve)); +} + +async function openInBrowser(url: string): Promise { + const { spawn } = await import('child_process'); + const platform = process.platform; + const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'; + try { + spawn(command, [url], { stdio: 'ignore', detached: true, shell: platform === 'win32' }).unref(); + } catch { + console.log('Could not auto-open a browser - open the URL above manually.'); + } +} diff --git a/src/index.ts b/src/index.ts index 8868b75..a156ceb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import { PrivacyScanner } from './scanner/engine.js'; import { BUILTIN_RULES, loadExternalRulesFromJson } from './scanner/detectors.js'; import { Redactor } from './redactor/masker.js'; import { ConfigLoader } from './config/loader.js'; +import { DetectionRule } from './types/findings.js'; /** * Main MCP server @@ -27,6 +28,8 @@ class PrivacyGuardServer { private server: Server; private scanner: PrivacyScanner; private config: ReturnType; + private allRules: DetectionRule[]; + private disabledRules: Set; constructor() { this.server = new Server( @@ -52,8 +55,9 @@ class PrivacyGuardServer { const externalRules = loadExternalRulesFromJson(externalRulesPath, { codingOnly: this.config.externalRulesMode !== 'all', }); - const disabledRules = new Set(this.config.disabledRules); - const activeRules = [...BUILTIN_RULES, ...externalRules].filter((rule) => !disabledRules.has(rule.id)); + this.allRules = [...BUILTIN_RULES, ...externalRules]; + this.disabledRules = new Set(this.config.disabledRules); + const activeRules = this.allRules.filter((rule) => !this.disabledRules.has(rule.id)); this.scanner = new PrivacyScanner(activeRules); this.setupHandlers(); @@ -98,6 +102,14 @@ class PrivacyGuardServer { required: ['text'], }, }, + { + name: 'list_rules', + description: 'List every detection rule (built-in and external) with its ID, title, severity, category, and whether it is currently disabled via disabledRules', + inputSchema: { + type: 'object', + properties: {}, + }, + }, ], })); @@ -146,6 +158,26 @@ class PrivacyGuardServer { }; } + case 'list_rules': { + const rules = this.allRules.map((rule) => ({ + id: rule.id, + title: rule.title, + severity: rule.severity, + category: rule.category, + source: rule.id.startsWith('external-') ? 'external' : 'builtin', + disabled: this.disabledRules.has(rule.id), + })); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ rules, total: rules.length, disabledCount: this.disabledRules.size }, null, 2), + }, + ], + }; + } + default: throw new Error(`Unknown tool: ${name}`); } diff --git a/src/scanner/detectors.ts b/src/scanner/detectors.ts index dc9050d..3083c0a 100644 --- a/src/scanner/detectors.ts +++ b/src/scanner/detectors.ts @@ -4,6 +4,7 @@ import { DetectionRule } from '../types/findings.js'; import * as fs from 'fs'; +import { createHash } from 'crypto'; interface ExternalRegexEntry { name: string; @@ -269,11 +270,13 @@ export function loadExternalRulesFromJson( continue; } - let id = `external-${slugify(entry.name)}`; - let suffix = 2; - while (usedIds.has(id)) { - id = `external-${slugify(entry.name)}-${suffix}`; - suffix += 1; + // Suffix with a short hash of the entry's own content (not its position) + // so IDs stay stable across data-file reordering, even on name collisions. + const baseId = `external-${slugify(entry.name)}`; + let id = baseId; + if (usedIds.has(id)) { + const contentHash = createHash('sha1').update(`${entry.name}:${entry.regex}`).digest('hex').slice(0, 6); + id = `${baseId}-${contentHash}`; } usedIds.add(id);