@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.
- package/.github/workflows/opencode-review.yml +52 -7
- package/dist/commands/init.d.ts +3 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +80 -26
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/redact-hook.d.ts +13 -4
- package/dist/commands/redact-hook.d.ts.map +1 -1
- package/dist/commands/redact-hook.js +162 -26
- package/dist/commands/redact-hook.js.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/opencode-hush.d.ts +15 -4
- package/dist/plugins/opencode-hush.d.ts.map +1 -1
- package/dist/plugins/opencode-hush.js +37 -4
- package/dist/plugins/opencode-hush.js.map +1 -1
- package/examples/team-config/.claude/settings.json +22 -0
- package/examples/team-config/.gemini/settings.json +38 -0
- package/examples/team-config/.opencode/plugins/hush.ts +9 -6
- package/package.json +1 -1
- package/src/commands/init.ts +110 -31
- package/src/commands/redact-hook.ts +206 -33
- package/src/index.ts +7 -2
- package/src/plugins/opencode-hush.ts +44 -4
- package/tests/init.test.ts +159 -5
- package/tests/opencode-plugin.test.ts +71 -0
- package/tests/redact-hook.test.ts +356 -0
|
@@ -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
|
-
*
|
|
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
|
|
9
|
-
* that slips through
|
|
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
|
|
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
|
-
*
|
|
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
|
|
9
|
-
* that slips through
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
* and add to `opencode.json`:
|
|
9
|
-
* { "plugin": [".opencode/plugins/hush.ts"] }
|
|
8
|
+
* npm install @aictrl/hush
|
|
10
9
|
*
|
|
11
|
-
*
|
|
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
package/src/commands/init.ts
CHANGED
|
@@ -1,87 +1,163 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hush init — Generate Claude Code
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
interface HookConfig {
|
|
79
|
+
hooks: Record<string, HookEntry[]>;
|
|
80
|
+
}
|
|
43
81
|
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
merged.hooks
|
|
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
|
|
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
|
|
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(
|
|
156
|
+
const filePath = join(configDir, filename);
|
|
81
157
|
|
|
82
|
-
// Ensure
|
|
83
|
-
if (!existsSync(
|
|
84
|
-
mkdirSync(
|
|
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
|
-
|
|
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
|
}
|