sovr-mcp-proxy 6.0.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -9,6 +9,10 @@ async function main(args) {
9
9
  await initCli(args.slice(1));
10
10
  return;
11
11
  }
12
+ case "hooks": {
13
+ await hooksCli(args.slice(1));
14
+ return;
15
+ }
12
16
  case "keys": {
13
17
  const { keysCli } = await import("./apiKeyManager.mjs");
14
18
  await keysCli(args.slice(1));
@@ -52,15 +56,151 @@ async function main(args) {
52
56
  }
53
57
  }
54
58
  }
59
+ async function hooksCli(args) {
60
+ const { installHooks, uninstallHooks, detectClaudeCode, generateHooksConfig } = await import("./hooksAdapter.mjs");
61
+ const subcommand = args[0] || "install";
62
+ let failMode = "open";
63
+ let sovrEndpoint = "http://localhost:3271";
64
+ let projectDir = process.cwd();
65
+ for (let i = 1; i < args.length; i++) {
66
+ if (args[i] === "--fail-closed") failMode = "closed";
67
+ if (args[i] === "--fail-open") failMode = "open";
68
+ if (args[i] === "--endpoint" && args[i + 1]) sovrEndpoint = args[++i];
69
+ if (args[i] === "--dir" && args[i + 1]) projectDir = args[++i];
70
+ }
71
+ const config = {
72
+ endpoint: sovrEndpoint,
73
+ failMode,
74
+ toolPatterns: ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit"],
75
+ addAuditHook: true
76
+ };
77
+ switch (subcommand) {
78
+ case "install": {
79
+ const hooksConfig = generateHooksConfig(config);
80
+ const { generateHookScript, generateAuditHookScript } = await import("./hooksAdapter.mjs");
81
+ const preToolScript = generateHookScript(config);
82
+ const postToolScript = config.addAuditHook ? generateAuditHookScript(config) : null;
83
+ const fs = await import("fs");
84
+ const path = await import("path");
85
+ const settingsDir = path.join(projectDir, ".claude");
86
+ const settingsFile = path.join(settingsDir, "settings.json");
87
+ if (!fs.existsSync(settingsDir)) {
88
+ fs.mkdirSync(settingsDir, { recursive: true });
89
+ }
90
+ let existingSettings = {};
91
+ if (fs.existsSync(settingsFile)) {
92
+ try {
93
+ existingSettings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
94
+ } catch {
95
+ }
96
+ }
97
+ const newSettings = {
98
+ ...existingSettings,
99
+ hooks: hooksConfig
100
+ };
101
+ fs.writeFileSync(settingsFile, JSON.stringify(newSettings, null, 2));
102
+ const scriptsDir = path.join(settingsDir, "hooks");
103
+ if (!fs.existsSync(scriptsDir)) {
104
+ fs.mkdirSync(scriptsDir, { recursive: true });
105
+ }
106
+ fs.writeFileSync(
107
+ path.join(scriptsDir, "sovr-pre-tool.sh"),
108
+ preToolScript,
109
+ { mode: 493 }
110
+ );
111
+ if (postToolScript) {
112
+ fs.writeFileSync(
113
+ path.join(scriptsDir, "sovr-post-tool.sh"),
114
+ postToolScript,
115
+ { mode: 493 }
116
+ );
117
+ }
118
+ process.stderr.write(`
119
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
120
+ \u2551 SOVR Claude Code Hooks Installed \u2551
121
+ \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
122
+ \u2551 \u2551
123
+ \u2551 Settings: ${settingsFile.padEnd(47)}\u2551
124
+ \u2551 Scripts: ${scriptsDir.padEnd(47)}\u2551
125
+ \u2551 Fail mode: ${failMode.padEnd(47)}\u2551
126
+ \u2551 Endpoint: ${sovrEndpoint.padEnd(47)}\u2551
127
+ \u2551 \u2551
128
+ \u2551 Every Bash/Write/Edit call will now pass through SOVR. \u2551
129
+ \u2551 Use 'sovr-mcp-proxy hooks status' to verify. \u2551
130
+ \u2551 \u2551
131
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
132
+ `);
133
+ break;
134
+ }
135
+ case "uninstall": {
136
+ const fs = await import("fs");
137
+ const path = await import("path");
138
+ const settingsFile = path.join(projectDir, ".claude", "settings.json");
139
+ if (fs.existsSync(settingsFile)) {
140
+ try {
141
+ const settings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
142
+ delete settings.hooks;
143
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
144
+ process.stderr.write("[SOVR] Hooks removed from .claude/settings.json\n");
145
+ } catch (err) {
146
+ process.stderr.write(`[SOVR] Error removing hooks: ${err.message}
147
+ `);
148
+ }
149
+ }
150
+ const scriptsDir = path.join(projectDir, ".claude", "hooks");
151
+ for (const f of ["sovr-pre-tool.sh", "sovr-post-tool.sh"]) {
152
+ const fp = path.join(scriptsDir, f);
153
+ if (fs.existsSync(fp)) fs.unlinkSync(fp);
154
+ }
155
+ process.stderr.write("[SOVR] Hooks uninstalled successfully.\n");
156
+ break;
157
+ }
158
+ case "status": {
159
+ const fs = await import("fs");
160
+ const path = await import("path");
161
+ const settingsFile = path.join(projectDir, ".claude", "settings.json");
162
+ if (!fs.existsSync(settingsFile)) {
163
+ process.stderr.write("[SOVR] No .claude/settings.json found. Hooks not installed.\n");
164
+ return;
165
+ }
166
+ try {
167
+ const settings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
168
+ if (settings.hooks) {
169
+ const preHooks = settings.hooks.PreToolUse || [];
170
+ const postHooks = settings.hooks.PostToolUse || [];
171
+ process.stderr.write(`[SOVR] Hooks Status:
172
+ `);
173
+ process.stderr.write(` PreToolUse hooks: ${preHooks.length}
174
+ `);
175
+ process.stderr.write(` PostToolUse hooks: ${postHooks.length}
176
+ `);
177
+ process.stderr.write(` Settings file: ${settingsFile}
178
+ `);
179
+ } else {
180
+ process.stderr.write("[SOVR] No hooks configured in settings.json\n");
181
+ }
182
+ } catch (err) {
183
+ process.stderr.write(`[SOVR] Error reading settings: ${err.message}
184
+ `);
185
+ }
186
+ break;
187
+ }
188
+ default:
189
+ process.stderr.write(`Unknown hooks subcommand: ${subcommand}
190
+ Usage: sovr-mcp-proxy hooks [install|uninstall|status]
191
+ `);
192
+ }
193
+ }
55
194
  function printHelp() {
56
195
  process.stderr.write(`
57
- sovr-mcp-proxy \u2014 The Responsibility Layer for AI Agents
196
+ sovr-mcp-proxy v7.0.0 \u2014 The Responsibility Layer for AI Agents
58
197
 
59
198
  USAGE:
60
199
  sovr-mcp-proxy [COMMAND] [OPTIONS]
61
200
 
62
201
  COMMANDS:
63
202
  init One-command setup for AI coding platforms
203
+ hooks Install/manage Claude Code Hooks (PreToolUse)
64
204
  keys Manage SOVR API keys
65
205
  usage View usage statistics
66
206
  daemon Manage persistent background daemon
@@ -68,10 +208,25 @@ COMMANDS:
68
208
 
69
209
  PROXY OPTIONS (default mode):
70
210
  --upstream, -u Downstream MCP server command to wrap
211
+ --mode, -m Security mode: exclusive|enforce|advisory|monitor
212
+ exclusive \u2014 Replace all native tools (strongest)
213
+ enforce \u2014 Intercept + block violations (default)
214
+ advisory \u2014 Warn but don't block
215
+ monitor \u2014 Silent audit only
216
+ --whitelist, -w Whitelist preset or path: readonly|developer|production|<file>
71
217
  --rules, -r Path to custom policy rules file
72
218
  --timeout, -t Startup timeout in ms (default: 30000)
73
219
  --verbose, -v Verbose output
74
220
 
221
+ HOOKS OPTIONS:
222
+ install Install Claude Code PreToolUse hooks (default)
223
+ uninstall Remove SOVR hooks from .claude/settings.json
224
+ status Show current hooks configuration
225
+ --fail-closed Block on SOVR unreachable (default: fail-open)
226
+ --fail-open Allow on SOVR unreachable
227
+ --endpoint URL SOVR daemon endpoint (default: http://localhost:3271)
228
+ --dir PATH Project directory (default: cwd)
229
+
75
230
  INIT OPTIONS:
76
231
  --claude-code Configure Claude Code
77
232
  --claude-desktop Configure Claude Desktop
@@ -100,31 +255,43 @@ DAEMON OPTIONS:
100
255
  --foreground Run in foreground (for debugging)
101
256
  --verbose Verbose logging
102
257
 
103
- EXAMPLES:
104
- # One-command setup (auto-detect platforms)
105
- npx sovr-mcp-proxy init
258
+ SECURITY MODES:
259
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
260
+ \u2502 exclusive \u2502 SOVR replaces native Bash/Shell tools. \u2502
261
+ \u2502 \u2502 LLM can ONLY execute through sovr_exec. \u2502
262
+ \u2502 \u2502 Bypass is architecturally impossible. \u2502
263
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
264
+ \u2502 enforce \u2502 Native tools preserved but all calls intercepted.\u2502
265
+ \u2502 \u2502 Violations are blocked with error response. \u2502
266
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
267
+ \u2502 advisory \u2502 Violations trigger warnings in stderr. \u2502
268
+ \u2502 \u2502 Calls are still forwarded to upstream. \u2502
269
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
270
+ \u2502 monitor \u2502 Silent audit logging only. \u2502
271
+ \u2502 \u2502 No warnings, no blocking. \u2502
272
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
106
273
 
107
- # Setup for Cursor with API key
108
- npx sovr-mcp-proxy init --cursor --api-key sovr_sk_abc123
274
+ WHITELIST PRESETS:
275
+ readonly \u2014 git status/log/diff, ls, cat, find, grep (read-only ops)
276
+ developer \u2014 readonly + git add/commit/push, npm/pnpm, tsc, eslint
277
+ production \u2014 developer + docker, kubectl, terraform (with restrictions)
109
278
 
110
- # Start proxy wrapping another MCP server
111
- npx sovr-mcp-proxy --upstream "npx -y @modelcontextprotocol/server-filesystem /tmp"
279
+ EXAMPLES:
280
+ # Maximum security: exclusive mode + readonly whitelist
281
+ npx sovr-mcp-proxy --upstream "npx @mcp/server-fs /tmp" --mode=exclusive --whitelist=readonly
112
282
 
113
- # Check usage
114
- npx sovr-mcp-proxy usage
283
+ # Developer workflow: enforce mode + developer whitelist
284
+ npx sovr-mcp-proxy --upstream "npx @mcp/server-fs ." --mode=enforce --whitelist=developer
115
285
 
116
- # Manage keys
117
- npx sovr-mcp-proxy keys add sovr_sk_abc123
286
+ # Install Claude Code hooks (no upstream needed)
287
+ npx sovr-mcp-proxy hooks install --fail-closed
118
288
 
119
- # Start daemon (eliminates cold-start overhead)
120
- npx sovr-mcp-proxy daemon start
289
+ # One-command setup (auto-detect platforms)
290
+ npx sovr-mcp-proxy init
121
291
 
122
- # Start daemon with fail-closed mode
292
+ # Start daemon with fail-closed
123
293
  npx sovr-mcp-proxy daemon start --fail-closed
124
294
 
125
- # Check daemon status
126
- npx sovr-mcp-proxy daemon status
127
-
128
295
  ENVIRONMENT:
129
296
  SOVR_API_KEY API key for authentication
130
297
  SOVR_ENDPOINT Custom SOVR endpoint URL
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @sovr/proxy-mcp — Command Normalizer
3
+ *
4
+ * Solves the Reddit feedback:
5
+ * "If you ban `rm`, I can just `bash -c 'rm -rf /'`" — coloradical5280
6
+ * "find / -delete, TRUNCATE TABLE, etc." — multiple users
7
+ *
8
+ * The normalizer decomposes complex commands into atomic operations
9
+ * that can each be individually evaluated against the whitelist.
10
+ *
11
+ * Key capabilities:
12
+ * 1. Unwrap shell wrappers: bash -c, sh -c, env, xargs, etc.
13
+ * 2. Split pipe chains: cmd1 | cmd2 | cmd3 → [cmd1, cmd2, cmd3]
14
+ * 3. Split command chains: cmd1 && cmd2; cmd3 → [cmd1, cmd2, cmd3]
15
+ * 4. Detect encoding tricks: base64 -d, hex encoding, etc.
16
+ * 5. Detect alias/function tricks: alias rm='rm -rf'
17
+ * 6. Extract the "effective command" from each segment
18
+ *
19
+ * Every segment must pass the whitelist independently.
20
+ * If ANY segment fails, the entire command is blocked.
21
+ *
22
+ * @example
23
+ * ```
24
+ * normalize('bash -c "rm -rf /tmp && echo done"')
25
+ * → [
26
+ * { raw: 'bash -c "rm -rf /tmp && echo done"', effective: 'rm -rf /tmp', type: 'unwrapped' },
27
+ * { raw: 'bash -c "rm -rf /tmp && echo done"', effective: 'echo done', type: 'chain-segment' },
28
+ * ]
29
+ * ```
30
+ */
31
+ /** A normalized command segment */
32
+ interface NormalizedSegment {
33
+ /** The original raw command (or the segment of it) */
34
+ raw: string;
35
+ /** The effective command after normalization */
36
+ effective: string;
37
+ /** How this segment was derived */
38
+ type: 'direct' | 'unwrapped' | 'pipe-segment' | 'chain-segment' | 'subshell' | 'heredoc' | 'encoded';
39
+ /** Warning flags */
40
+ warnings: string[];
41
+ }
42
+ /** Result of normalization */
43
+ interface NormalizationResult {
44
+ /** All command segments that need individual evaluation */
45
+ segments: NormalizedSegment[];
46
+ /** Whether the command structure itself is suspicious */
47
+ suspicious: boolean;
48
+ /** Reasons for suspicion */
49
+ suspicion_reasons: string[];
50
+ /** The original command */
51
+ original: string;
52
+ }
53
+ /**
54
+ * Normalize a command into atomic segments for individual evaluation.
55
+ *
56
+ * This is the main entry point. It:
57
+ * 1. Splits command chains (&&, ||, ;)
58
+ * 2. Splits pipe chains (|)
59
+ * 3. Unwraps shell wrappers (bash -c, sudo, env, etc.)
60
+ * 4. Detects encoding tricks
61
+ * 5. Detects destructive equivalents
62
+ *
63
+ * Returns all segments that need to be individually evaluated.
64
+ */
65
+ declare function normalize(command: string): NormalizationResult;
66
+ /**
67
+ * Get the base command name (first word) from a command string.
68
+ */
69
+ declare function getBaseCommand(command: string): string;
70
+ /**
71
+ * Check if a command contains any destructive equivalent patterns.
72
+ */
73
+ declare function hasDestructiveEquivalent(command: string): {
74
+ found: boolean;
75
+ matches: Array<{
76
+ equivalent: string;
77
+ description: string;
78
+ }>;
79
+ };
80
+ /**
81
+ * Check if a command uses any encoding/obfuscation tricks.
82
+ */
83
+ declare function hasEncodingTricks(command: string): {
84
+ found: boolean;
85
+ tricks: Array<{
86
+ name: string;
87
+ risk: string;
88
+ }>;
89
+ };
90
+ /**
91
+ * Summarize normalization result in a human-readable format.
92
+ */
93
+ declare function summarize(result: NormalizationResult): string;
94
+
95
+ export { type NormalizationResult, type NormalizedSegment, getBaseCommand, hasDestructiveEquivalent, hasEncodingTricks, normalize, summarize };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @sovr/proxy-mcp — Command Normalizer
3
+ *
4
+ * Solves the Reddit feedback:
5
+ * "If you ban `rm`, I can just `bash -c 'rm -rf /'`" — coloradical5280
6
+ * "find / -delete, TRUNCATE TABLE, etc." — multiple users
7
+ *
8
+ * The normalizer decomposes complex commands into atomic operations
9
+ * that can each be individually evaluated against the whitelist.
10
+ *
11
+ * Key capabilities:
12
+ * 1. Unwrap shell wrappers: bash -c, sh -c, env, xargs, etc.
13
+ * 2. Split pipe chains: cmd1 | cmd2 | cmd3 → [cmd1, cmd2, cmd3]
14
+ * 3. Split command chains: cmd1 && cmd2; cmd3 → [cmd1, cmd2, cmd3]
15
+ * 4. Detect encoding tricks: base64 -d, hex encoding, etc.
16
+ * 5. Detect alias/function tricks: alias rm='rm -rf'
17
+ * 6. Extract the "effective command" from each segment
18
+ *
19
+ * Every segment must pass the whitelist independently.
20
+ * If ANY segment fails, the entire command is blocked.
21
+ *
22
+ * @example
23
+ * ```
24
+ * normalize('bash -c "rm -rf /tmp && echo done"')
25
+ * → [
26
+ * { raw: 'bash -c "rm -rf /tmp && echo done"', effective: 'rm -rf /tmp', type: 'unwrapped' },
27
+ * { raw: 'bash -c "rm -rf /tmp && echo done"', effective: 'echo done', type: 'chain-segment' },
28
+ * ]
29
+ * ```
30
+ */
31
+ /** A normalized command segment */
32
+ interface NormalizedSegment {
33
+ /** The original raw command (or the segment of it) */
34
+ raw: string;
35
+ /** The effective command after normalization */
36
+ effective: string;
37
+ /** How this segment was derived */
38
+ type: 'direct' | 'unwrapped' | 'pipe-segment' | 'chain-segment' | 'subshell' | 'heredoc' | 'encoded';
39
+ /** Warning flags */
40
+ warnings: string[];
41
+ }
42
+ /** Result of normalization */
43
+ interface NormalizationResult {
44
+ /** All command segments that need individual evaluation */
45
+ segments: NormalizedSegment[];
46
+ /** Whether the command structure itself is suspicious */
47
+ suspicious: boolean;
48
+ /** Reasons for suspicion */
49
+ suspicion_reasons: string[];
50
+ /** The original command */
51
+ original: string;
52
+ }
53
+ /**
54
+ * Normalize a command into atomic segments for individual evaluation.
55
+ *
56
+ * This is the main entry point. It:
57
+ * 1. Splits command chains (&&, ||, ;)
58
+ * 2. Splits pipe chains (|)
59
+ * 3. Unwraps shell wrappers (bash -c, sudo, env, etc.)
60
+ * 4. Detects encoding tricks
61
+ * 5. Detects destructive equivalents
62
+ *
63
+ * Returns all segments that need to be individually evaluated.
64
+ */
65
+ declare function normalize(command: string): NormalizationResult;
66
+ /**
67
+ * Get the base command name (first word) from a command string.
68
+ */
69
+ declare function getBaseCommand(command: string): string;
70
+ /**
71
+ * Check if a command contains any destructive equivalent patterns.
72
+ */
73
+ declare function hasDestructiveEquivalent(command: string): {
74
+ found: boolean;
75
+ matches: Array<{
76
+ equivalent: string;
77
+ description: string;
78
+ }>;
79
+ };
80
+ /**
81
+ * Check if a command uses any encoding/obfuscation tricks.
82
+ */
83
+ declare function hasEncodingTricks(command: string): {
84
+ found: boolean;
85
+ tricks: Array<{
86
+ name: string;
87
+ risk: string;
88
+ }>;
89
+ };
90
+ /**
91
+ * Summarize normalization result in a human-readable format.
92
+ */
93
+ declare function summarize(result: NormalizationResult): string;
94
+
95
+ export { type NormalizationResult, type NormalizedSegment, getBaseCommand, hasDestructiveEquivalent, hasEncodingTricks, normalize, summarize };