@aictrl/hush 0.1.7 → 0.1.8

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.
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * OpenCode Plugin: Hush PII Guard
3
3
  *
4
- * Blocks reads of sensitive files (`.env`, `*.pem`, `credentials.*`, etc.)
5
- * before the tool executes — the AI model never sees the content.
4
+ * 1. Blocks reads of sensitive files (`.env`, `*.pem`, `credentials.*`, etc.)
5
+ * before the tool executes — the AI model never sees the content.
6
+ * 2. Redacts PII (emails, IPs, secrets) from tool arguments before execution.
7
+ * 3. Redacts PII from tool outputs (built-in and MCP) after execution.
6
8
  *
7
9
  * Defense-in-depth: works alongside the Hush proxy which redacts PII from
8
- * API requests. The plugin prevents file reads; the proxy catches anything
9
- * that slips through in normal files.
10
+ * API requests. The plugin prevents file reads and scrubs tool I/O;
11
+ * the proxy catches anything that slips through.
10
12
  *
11
13
  * Install: copy to `.opencode/plugins/hush.ts` and add to `opencode.json`:
12
14
  * { "plugin": [".opencode/plugins/hush.ts"] }
@@ -17,5 +19,14 @@ export declare const HushPlugin: () => Promise<{
17
19
  }, output: {
18
20
  args: Record<string, string>;
19
21
  }) => Promise<void>;
22
+ 'tool.execute.after': (input: {
23
+ tool: string;
24
+ }, output: {
25
+ output?: string;
26
+ content?: Array<{
27
+ type: string;
28
+ text?: string;
29
+ }>;
30
+ }) => Promise<void>;
20
31
  }>;
21
32
  //# sourceMappingURL=opencode-hush.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-hush.d.ts","sourceRoot":"","sources":["../../src/plugins/opencode-hush.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,UAAU;mCAEZ;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,UACf;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;EAU1C,CAAC"}
1
+ {"version":3,"file":"opencode-hush.d.ts","sourceRoot":"","sources":["../../src/plugins/opencode-hush.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,eAAO,MAAM,UAAU;mCAEZ;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,UACf;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;kCAsBjC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,UACf;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE;EAsB/E,CAAC"}
@@ -1,25 +1,58 @@
1
1
  /**
2
2
  * OpenCode Plugin: Hush PII Guard
3
3
  *
4
- * Blocks reads of sensitive files (`.env`, `*.pem`, `credentials.*`, etc.)
5
- * before the tool executes — the AI model never sees the content.
4
+ * 1. Blocks reads of sensitive files (`.env`, `*.pem`, `credentials.*`, etc.)
5
+ * before the tool executes — the AI model never sees the content.
6
+ * 2. Redacts PII (emails, IPs, secrets) from tool arguments before execution.
7
+ * 3. Redacts PII from tool outputs (built-in and MCP) after execution.
6
8
  *
7
9
  * Defense-in-depth: works alongside the Hush proxy which redacts PII from
8
- * API requests. The plugin prevents file reads; the proxy catches anything
9
- * that slips through in normal files.
10
+ * API requests. The plugin prevents file reads and scrubs tool I/O;
11
+ * the proxy catches anything that slips through.
10
12
  *
11
13
  * Install: copy to `.opencode/plugins/hush.ts` and add to `opencode.json`:
12
14
  * { "plugin": [".opencode/plugins/hush.ts"] }
13
15
  */
14
16
  import { isSensitivePath, commandReadsSensitiveFile } from './sensitive-patterns.js';
17
+ import { Redactor } from '../middleware/redactor.js';
18
+ const redactor = new Redactor();
15
19
  export const HushPlugin = async () => ({
16
20
  'tool.execute.before': async (input, output) => {
21
+ // Block sensitive file reads first (hard block — throws)
17
22
  if (input.tool === 'read' && isSensitivePath(output.args['filePath'] ?? '')) {
18
23
  throw new Error('[hush] Blocked: sensitive file');
19
24
  }
20
25
  if (input.tool === 'bash' && commandReadsSensitiveFile(output.args['command'] ?? '')) {
21
26
  throw new Error('[hush] Blocked: command reads sensitive file');
22
27
  }
28
+ // Redact PII from outbound tool arguments (in-place mutation)
29
+ const { content, hasRedacted } = redactor.redact(output.args);
30
+ if (hasRedacted) {
31
+ const redacted = content;
32
+ for (const key of Object.keys(redacted)) {
33
+ output.args[key] = redacted[key];
34
+ }
35
+ }
36
+ },
37
+ 'tool.execute.after': async (input, output) => {
38
+ // Built-in tools: output is a string at output.output
39
+ if (typeof output.output === 'string') {
40
+ const { content, hasRedacted } = redactor.redact(output.output);
41
+ if (hasRedacted) {
42
+ output.output = content;
43
+ }
44
+ }
45
+ // MCP tools: output is content blocks at output.content
46
+ if (Array.isArray(output.content)) {
47
+ for (const block of output.content) {
48
+ if (block.type === 'text' && typeof block.text === 'string') {
49
+ const { content, hasRedacted } = redactor.redact(block.text);
50
+ if (hasRedacted) {
51
+ block.text = content;
52
+ }
53
+ }
54
+ }
55
+ }
23
56
  },
24
57
  });
25
58
  //# sourceMappingURL=opencode-hush.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-hush.js","sourceRoot":"","sources":["../../src/plugins/opencode-hush.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAErF,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC;IACrC,qBAAqB,EAAE,KAAK,EAC1B,KAAuB,EACvB,MAAwC,EACxC,EAAE;QACF,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACpD,CAAC;QAED,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,yBAAyB,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACrF,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"opencode-hush.js","sourceRoot":"","sources":["../../src/plugins/opencode-hush.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAErD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;AAEhC,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC;IACrC,qBAAqB,EAAE,KAAK,EAC1B,KAAuB,EACvB,MAAwC,EACxC,EAAE;QACF,yDAAyD;QACzD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACpD,CAAC;QAED,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,yBAAyB,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACrF,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAClE,CAAC;QAED,8DAA8D;QAC9D,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC9D,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,QAAQ,GAAG,OAAiC,CAAC;YACnD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAE,CAAC;YACpC,CAAC;QACH,CAAC;IACH,CAAC;IAED,oBAAoB,EAAE,KAAK,EACzB,KAAuB,EACvB,MAA6E,EAC7E,EAAE;QACF,sDAAsD;QACtD,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACtC,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAChE,IAAI,WAAW,EAAE,CAAC;gBAChB,MAAM,CAAC,MAAM,GAAG,OAAiB,CAAC;YACpC,CAAC;QACH,CAAC;QAED,wDAAwD;QACxD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAClC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC5D,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBAC7D,IAAI,WAAW,EAAE,CAAC;wBAChB,KAAK,CAAC,IAAI,GAAG,OAAiB,CAAC;oBACjC,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;CACF,CAAC,CAAC"}
@@ -3,6 +3,18 @@
3
3
  "ANTHROPIC_BASE_URL": "http://127.0.0.1:4000"
4
4
  },
5
5
  "hooks": {
6
+ "PreToolUse": [
7
+ {
8
+ "matcher": "mcp__.*",
9
+ "hooks": [
10
+ {
11
+ "type": "command",
12
+ "command": "hush redact-hook",
13
+ "timeout": 10
14
+ }
15
+ ]
16
+ }
17
+ ],
6
18
  "PostToolUse": [
7
19
  {
8
20
  "matcher": "Bash|Read|Grep|WebFetch",
@@ -13,6 +25,16 @@
13
25
  "timeout": 10
14
26
  }
15
27
  ]
28
+ },
29
+ {
30
+ "matcher": "mcp__.*",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "hush redact-hook",
35
+ "timeout": 10
36
+ }
37
+ ]
16
38
  }
17
39
  ]
18
40
  }
@@ -0,0 +1,38 @@
1
+ {
2
+ "hooks": {
3
+ "BeforeTool": [
4
+ {
5
+ "matcher": "mcp__.*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "hush redact-hook",
10
+ "timeout": 10
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "AfterTool": [
16
+ {
17
+ "matcher": "run_shell_command|read_file|read_many_files|search_file_content|web_fetch",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "hush redact-hook",
22
+ "timeout": 10
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "matcher": "mcp__.*",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "hush redact-hook",
32
+ "timeout": 10
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+ }
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Hush PII Guard — OpenCode Plugin (drop-in copy)
3
3
  *
4
- * Blocks reads of sensitive files (.env, *.pem, credentials.*, etc.)
5
- * before the tool executes the AI model never sees the content.
4
+ * This drop-in copy provides file-blocking only (sensitive file reads).
5
+ * For full bidirectional PII redaction (tool args + tool results),
6
+ * install from npm instead:
6
7
  *
7
- * Usage: copy this file to `.opencode/plugins/hush.ts` in your project
8
- * and add to `opencode.json`:
9
- * { "plugin": [".opencode/plugins/hush.ts"] }
8
+ * npm install @aictrl/hush
10
9
  *
11
- * Or install from npm:
10
+ * Then in your plugin entry point:
12
11
  * import { HushPlugin } from '@aictrl/hush/opencode-plugin'
12
+ *
13
+ * Usage (drop-in): copy this file to `.opencode/plugins/hush.ts` in your
14
+ * project and add to `opencode.json`:
15
+ * { "plugin": [".opencode/plugins/hush.ts"] }
13
16
  */
14
17
 
15
18
  const SENSITIVE_GLOBS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aictrl/hush",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Hush: A Semantic Security Gateway for AI Agents. Redacts PII from prompts and tool outputs locally before they hit the cloud.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,87 +1,163 @@
1
1
  /**
2
- * hush init — Generate Claude Code hook configuration
2
+ * hush init — Generate hook configuration for Claude Code or Gemini CLI
3
3
  *
4
4
  * Usage:
5
5
  * hush init --hooks Write to .claude/settings.json
6
6
  * hush init --hooks --local Write to .claude/settings.local.json
7
+ * hush init --hooks --gemini Write to .gemini/settings.json
8
+ * hush init --hooks --gemini --local Write to .gemini/settings.local.json
7
9
  */
8
10
 
9
11
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
10
12
  import { join } from 'path';
11
13
 
12
- const HOOK_CONFIG = {
14
+ const HUSH_HOOK = {
15
+ type: 'command' as const,
16
+ command: 'hush redact-hook',
17
+ timeout: 10,
18
+ };
19
+
20
+ const CLAUDE_HOOK_CONFIG = {
13
21
  hooks: {
22
+ PreToolUse: [
23
+ {
24
+ matcher: 'mcp__.*',
25
+ hooks: [HUSH_HOOK],
26
+ },
27
+ ],
14
28
  PostToolUse: [
15
29
  {
16
30
  matcher: 'Bash|Read|Grep|WebFetch',
17
- hooks: [
18
- {
19
- type: 'command' as const,
20
- command: 'hush redact-hook',
21
- timeout: 10,
22
- },
23
- ],
31
+ hooks: [HUSH_HOOK],
32
+ },
33
+ {
34
+ matcher: 'mcp__.*',
35
+ hooks: [HUSH_HOOK],
36
+ },
37
+ ],
38
+ },
39
+ };
40
+
41
+ const GEMINI_HOOK_CONFIG = {
42
+ hooks: {
43
+ BeforeTool: [
44
+ {
45
+ matcher: 'mcp__.*',
46
+ hooks: [HUSH_HOOK],
47
+ },
48
+ ],
49
+ AfterTool: [
50
+ {
51
+ matcher: 'run_shell_command|read_file|read_many_files|search_file_content|web_fetch',
52
+ hooks: [HUSH_HOOK],
53
+ },
54
+ {
55
+ matcher: 'mcp__.*',
56
+ hooks: [HUSH_HOOK],
24
57
  },
25
58
  ],
26
59
  },
27
60
  };
28
61
 
62
+ interface HookEntry {
63
+ matcher: string;
64
+ hooks: Array<{ type: string; command: string; timeout?: number }>;
65
+ }
66
+
29
67
  interface SettingsJson {
30
68
  hooks?: {
31
- PostToolUse?: Array<{
32
- matcher: string;
33
- hooks: Array<{ type: string; command: string; timeout?: number }>;
34
- }>;
69
+ PreToolUse?: HookEntry[];
70
+ PostToolUse?: HookEntry[];
71
+ BeforeTool?: HookEntry[];
72
+ AfterTool?: HookEntry[];
35
73
  [key: string]: unknown;
36
74
  };
37
75
  [key: string]: unknown;
38
76
  }
39
77
 
40
- function hasHushHook(settings: SettingsJson): boolean {
41
- const postToolUse = settings.hooks?.PostToolUse;
42
- if (!Array.isArray(postToolUse)) return false;
78
+ interface HookConfig {
79
+ hooks: Record<string, HookEntry[]>;
80
+ }
43
81
 
44
- return postToolUse.some((entry) =>
82
+ function hasHushHookInEntries(entries: HookEntry[] | undefined): boolean {
83
+ if (!Array.isArray(entries)) return false;
84
+ return entries.some((entry) =>
45
85
  entry.hooks?.some((h) => h.command?.includes('hush redact-hook')),
46
86
  );
47
87
  }
48
88
 
49
- function mergeHooks(existing: SettingsJson): SettingsJson {
89
+ function hasHushHookClaude(settings: SettingsJson): boolean {
90
+ return (
91
+ hasHushHookInEntries(settings.hooks?.PreToolUse) &&
92
+ hasHushHookInEntries(settings.hooks?.PostToolUse)
93
+ );
94
+ }
95
+
96
+ function hasHushHookGemini(settings: SettingsJson): boolean {
97
+ return (
98
+ hasHushHookInEntries(settings.hooks?.BeforeTool) &&
99
+ hasHushHookInEntries(settings.hooks?.AfterTool)
100
+ );
101
+ }
102
+
103
+ function mergeHookEntries(
104
+ existing: HookEntry[] | undefined,
105
+ newEntries: HookEntry[],
106
+ ): HookEntry[] {
107
+ const merged = Array.isArray(existing) ? [...existing] : [];
108
+
109
+ for (const entry of newEntries) {
110
+ const alreadyHas = merged.some(
111
+ (e) =>
112
+ e.matcher === entry.matcher &&
113
+ e.hooks?.some((h) => h.command?.includes('hush redact-hook')),
114
+ );
115
+ if (!alreadyHas) {
116
+ merged.push(entry);
117
+ }
118
+ }
119
+
120
+ return merged;
121
+ }
122
+
123
+ function mergeHooks(existing: SettingsJson, hookConfig: HookConfig): SettingsJson {
50
124
  const merged = { ...existing };
51
125
 
52
126
  if (!merged.hooks) {
53
127
  merged.hooks = {};
54
128
  }
55
129
 
56
- if (!Array.isArray(merged.hooks.PostToolUse)) {
57
- merged.hooks.PostToolUse = [];
130
+ for (const [eventName, entries] of Object.entries(hookConfig.hooks)) {
131
+ const existingEntries = merged.hooks[eventName] as HookEntry[] | undefined;
132
+ merged.hooks[eventName] = mergeHookEntries(existingEntries, entries);
58
133
  }
59
134
 
60
- merged.hooks = { ...merged.hooks, PostToolUse: [...merged.hooks.PostToolUse, ...HOOK_CONFIG.hooks.PostToolUse] };
61
-
62
135
  return merged;
63
136
  }
64
137
 
65
138
  export function run(args: string[]): void {
66
139
  const hasHooksFlag = args.includes('--hooks');
67
140
  const isLocal = args.includes('--local');
141
+ const isGemini = args.includes('--gemini');
68
142
 
69
143
  if (!hasHooksFlag) {
70
- process.stderr.write('Usage: hush init --hooks [--local]\n');
144
+ process.stderr.write('Usage: hush init --hooks [--local] [--gemini]\n');
71
145
  process.stderr.write('\n');
72
146
  process.stderr.write('Options:\n');
73
- process.stderr.write(' --hooks Generate Claude Code PostToolUse hook config\n');
147
+ process.stderr.write(' --hooks Generate hook config (PreToolUse + PostToolUse or BeforeTool + AfterTool)\n');
74
148
  process.stderr.write(' --local Write to settings.local.json instead of settings.json\n');
149
+ process.stderr.write(' --gemini Write Gemini CLI hooks instead of Claude Code hooks\n');
75
150
  process.exit(1);
76
151
  }
77
152
 
78
- const claudeDir = join(process.cwd(), '.claude');
153
+ const dirName = isGemini ? '.gemini' : '.claude';
154
+ const configDir = join(process.cwd(), dirName);
79
155
  const filename = isLocal ? 'settings.local.json' : 'settings.json';
80
- const filePath = join(claudeDir, filename);
156
+ const filePath = join(configDir, filename);
81
157
 
82
- // Ensure .claude/ exists
83
- if (!existsSync(claudeDir)) {
84
- mkdirSync(claudeDir, { recursive: true });
158
+ // Ensure config dir exists
159
+ if (!existsSync(configDir)) {
160
+ mkdirSync(configDir, { recursive: true });
85
161
  }
86
162
 
87
163
  // Read existing settings or start fresh
@@ -96,12 +172,15 @@ export function run(args: string[]): void {
96
172
  }
97
173
 
98
174
  // Idempotency check
99
- if (hasHushHook(settings)) {
175
+ const hookConfig = isGemini ? GEMINI_HOOK_CONFIG : CLAUDE_HOOK_CONFIG;
176
+ const hasHook = isGemini ? hasHushHookGemini : hasHushHookClaude;
177
+
178
+ if (hasHook(settings)) {
100
179
  process.stdout.write(`hush hooks already configured in ${filePath}\n`);
101
180
  return;
102
181
  }
103
182
 
104
- const merged = mergeHooks(settings);
183
+ const merged = mergeHooks(settings, hookConfig);
105
184
  writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n');
106
185
  process.stdout.write(`Wrote hush hooks config to ${filePath}\n`);
107
186
  }