@aictrl/hush 0.1.6 → 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.
Files changed (50) hide show
  1. package/.github/workflows/opencode-review.yml +52 -7
  2. package/.gitlab-ci.yml +59 -0
  3. package/README.md +150 -3
  4. package/dist/cli.js +30 -17
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/init.d.ts +11 -0
  7. package/dist/commands/init.d.ts.map +1 -0
  8. package/dist/commands/init.js +135 -0
  9. package/dist/commands/init.js.map +1 -0
  10. package/dist/commands/redact-hook.d.ts +21 -0
  11. package/dist/commands/redact-hook.d.ts.map +1 -0
  12. package/dist/commands/redact-hook.js +225 -0
  13. package/dist/commands/redact-hook.js.map +1 -0
  14. package/dist/index.js +8 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/middleware/redactor.d.ts +5 -0
  17. package/dist/middleware/redactor.d.ts.map +1 -1
  18. package/dist/middleware/redactor.js +69 -0
  19. package/dist/middleware/redactor.js.map +1 -1
  20. package/dist/plugins/opencode-hush.d.ts +32 -0
  21. package/dist/plugins/opencode-hush.d.ts.map +1 -0
  22. package/dist/plugins/opencode-hush.js +58 -0
  23. package/dist/plugins/opencode-hush.js.map +1 -0
  24. package/dist/plugins/sensitive-patterns.d.ts +15 -0
  25. package/dist/plugins/sensitive-patterns.d.ts.map +1 -0
  26. package/dist/plugins/sensitive-patterns.js +69 -0
  27. package/dist/plugins/sensitive-patterns.js.map +1 -0
  28. package/dist/vault/token-vault.d.ts.map +1 -1
  29. package/dist/vault/token-vault.js +16 -3
  30. package/dist/vault/token-vault.js.map +1 -1
  31. package/examples/team-config/.claude/settings.json +41 -0
  32. package/examples/team-config/.codex/config.toml +4 -0
  33. package/examples/team-config/.gemini/settings.json +38 -0
  34. package/examples/team-config/.opencode/plugins/hush.ts +79 -0
  35. package/examples/team-config/opencode.json +10 -0
  36. package/package.json +11 -1
  37. package/scripts/e2e-plugin-block.sh +142 -0
  38. package/scripts/e2e-proxy-live.sh +185 -0
  39. package/src/cli.ts +28 -16
  40. package/src/commands/init.ts +186 -0
  41. package/src/commands/redact-hook.ts +297 -0
  42. package/src/index.ts +7 -2
  43. package/src/middleware/redactor.ts +75 -0
  44. package/src/plugins/opencode-hush.ts +70 -0
  45. package/src/plugins/sensitive-patterns.ts +71 -0
  46. package/src/vault/token-vault.ts +18 -4
  47. package/tests/init.test.ts +255 -0
  48. package/tests/opencode-plugin.test.ts +219 -0
  49. package/tests/redact-hook.test.ts +498 -0
  50. package/tests/redaction.test.ts +96 -0
@@ -69,33 +69,78 @@ jobs:
69
69
  echo "skip=false" >> $GITHUB_OUTPUT
70
70
  fi
71
71
 
72
+ - name: Setup Node.js
73
+ if: steps.check_changes.outputs.skip != 'true'
74
+ uses: actions/setup-node@v4
75
+ with:
76
+ node-version: 22
77
+
78
+ - name: Start Hush Gateway
79
+ if: steps.check_changes.outputs.skip != 'true'
80
+ run: |
81
+ npm ci && npm run build
82
+ PORT=4000 HUSH_HOST=127.0.0.1 node dist/cli.js > /tmp/hush-gateway.log 2>&1 &
83
+ for i in $(seq 1 20); do
84
+ curl -sf http://127.0.0.1:4000/health > /dev/null 2>&1 && break
85
+ sleep 0.5
86
+ done
87
+ curl -sf http://127.0.0.1:4000/health || { echo "::error::Hush gateway failed to start"; exit 1; }
88
+ echo "Hush gateway running on :4000"
89
+
72
90
  - name: Setup OpenCode
73
91
  if: steps.check_changes.outputs.skip != 'true'
74
92
  env:
75
93
  ZHIPU_API_KEY: ${{ secrets.ZHIPUAI_API_KEY }}
76
94
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77
95
  run: |
78
- # Use GITHUB_TOKEN to avoid rate limits when fetching version info
79
96
  curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path
80
97
  echo "$HOME/.opencode/bin" >> $GITHUB_PATH
81
98
 
99
+ # Route the built-in zai-coding-plan provider through the hush proxy
100
+ # by overriding baseURL. No custom provider needed — the provider
101
+ # auto-detects apiKey from ZHIPU_API_KEY env var.
102
+ mkdir -p .opencode/plugins
103
+ cp examples/team-config/.opencode/plugins/hush.ts .opencode/plugins/hush.ts
104
+ printf '%s\n' '{"provider":{"zai-coding-plan":{"options":{"baseURL":"http://127.0.0.1:4000/api/coding/paas/v4"}}},"plugin":[".opencode/plugins/hush.ts"]}' > opencode.json
105
+
82
106
  - name: Direct OpenCode Review
83
107
  if: steps.check_changes.outputs.skip != 'true'
108
+ timeout-minutes: 15
84
109
  env:
85
110
  ZHIPU_API_KEY: ${{ secrets.ZHIPUAI_API_KEY }}
86
111
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87
112
  run: |
88
113
  SHA=${{ github.event.pull_request.head.sha || github.sha }}
89
- echo "Starting review with GLM-5 for SHA $SHA..."
90
-
91
- $HOME/.opencode/bin/opencode run --model zai-coding-plan/glm-5 "Review the changes in this PR for the Hush Semantic Gateway.
114
+ PR_NUMBER=${{ github.event.pull_request.number }}
115
+ echo "Starting review with zai-coding-plan/glm-5 via hush proxy for PR #$PR_NUMBER SHA $SHA..."
116
+ echo "opencode.json:"; cat opencode.json
117
+ echo "Hush health:"; curl -sf http://127.0.0.1:4000/health || echo "gateway unreachable"
118
+
119
+ $HOME/.opencode/bin/opencode run --model zai-coding-plan/glm-5 "Review the changes in PR #$PR_NUMBER for the Hush Semantic Gateway.
120
+
121
+ **IMPORTANT**: This is a code review only. Do NOT run tests, npm commands, or build commands. Only read source files and git diffs.
92
122
 
93
123
  Focus areas:
94
124
  1. **Redaction Logic**: Ensure PII patterns are robust and handle edge cases in tool outputs (like JSON or CLI tables).
95
125
  2. **Streaming Integrity**: Check that the SSE/streaming proxy logic doesn't buffer unnecessarily or break the rehydration flow.
96
126
  3. **Security**: Look for potential PII leaks or insecure token handling in the vault.
97
127
  4. **Reliability**: Ensure the proxy handles upstream errors gracefully.
98
-
99
- Keep the summary concise but technical. Post findings as a markdown comment on the PR.
100
-
128
+
129
+ Keep the review concise. Post findings as a single markdown comment using:
130
+ gh pr comment $PR_NUMBER --body \"<your review>\"
131
+
132
+ Do NOT try to auto-detect the PR number — use exactly $PR_NUMBER.
133
+
101
134
  **CRITICAL**: Include the string 'Reviewed SHA: $SHA' at the very end of your comment so I can track which commits have been reviewed."
135
+
136
+ - name: Verify Hush Proxy Was Used
137
+ if: always() && steps.check_changes.outputs.skip != 'true'
138
+ run: |
139
+ echo "=== Hush Gateway Logs ==="
140
+ cat /tmp/hush-gateway.log 2>/dev/null || echo "No gateway log found"
141
+ echo ""
142
+ if grep -q "Starting stream proxy" /tmp/hush-gateway.log 2>/dev/null; then
143
+ echo "Hush proxy intercepted traffic"
144
+ else
145
+ echo "::warning::No proxy traffic detected in hush logs — review may have bypassed the gateway"
146
+ fi
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,59 @@
1
+ stages:
2
+ - build
3
+ - e2e
4
+
5
+ build:
6
+ stage: build
7
+ image: node:22-slim
8
+ script:
9
+ - npm ci
10
+ - npm run build
11
+ - npm test
12
+ artifacts:
13
+ paths:
14
+ - dist/
15
+ expire_in: 1 hour
16
+ cache:
17
+ key:
18
+ files:
19
+ - package-lock.json
20
+ paths:
21
+ - node_modules/
22
+
23
+ e2e-plugin-blocks-env:
24
+ stage: e2e
25
+ image: node:22-slim
26
+ needs: [build]
27
+ cache:
28
+ key:
29
+ files:
30
+ - package-lock.json
31
+ paths:
32
+ - node_modules/
33
+ policy: pull
34
+ before_script:
35
+ - npm install -g opencode
36
+ script:
37
+ - chmod +x scripts/e2e-plugin-block.sh
38
+ - ./scripts/e2e-plugin-block.sh
39
+ variables:
40
+ ZHIPUAI_API_KEY: $ZHIPUAI_API_KEY
41
+
42
+ e2e-proxy-redacts-pii:
43
+ stage: e2e
44
+ image: node:22-slim
45
+ needs: [build]
46
+ cache:
47
+ key:
48
+ files:
49
+ - package-lock.json
50
+ paths:
51
+ - node_modules/
52
+ policy: pull
53
+ before_script:
54
+ - npm install -g opencode
55
+ script:
56
+ - chmod +x scripts/e2e-proxy-live.sh
57
+ - ./scripts/e2e-proxy-live.sh
58
+ variables:
59
+ ZHIPUAI_API_KEY: $ZHIPUAI_API_KEY
package/README.md CHANGED
@@ -57,12 +57,15 @@ Create `opencode.json` in your project root:
57
57
 
58
58
  ### Gemini CLI
59
59
 
60
- Gemini CLI only supports env vars for endpoint override (no settings file option):
60
+ Add `.gemini/.env` to your project root (or set the env var directly):
61
61
 
62
62
  ```bash
63
- CODE_ASSIST_ENDPOINT=http://127.0.0.1:4000 gemini
63
+ # .gemini/.env
64
+ CODE_ASSIST_ENDPOINT=http://127.0.0.1:4000
64
65
  ```
65
66
 
67
+ Or: `CODE_ASSIST_ENDPOINT=http://127.0.0.1:4000 gemini`
68
+
66
69
  ### Verify it works
67
70
 
68
71
  When your AI tool sends a request containing PII, the hush terminal shows:
@@ -73,6 +76,150 @@ INFO: Redacted sensitive data from request path="/v1/messages" tokenCount=2 d
73
76
 
74
77
  Your tool still sees the real data (rehydrated locally). The LLM provider only ever sees tokens like `[USER_EMAIL_f22c5a]`.
75
78
 
79
+ ## Enforce for Your Team
80
+
81
+ Commit config files to your repo so every developer automatically routes through hush — no manual setup per person.
82
+
83
+ Copy the files from [`examples/team-config/`](examples/team-config/) into your project root:
84
+
85
+ ```
86
+ your-project/
87
+ ├── .claude/settings.json # Claude Code → hush
88
+ ├── .codex/config.toml # Codex → hush
89
+ ├── .gemini/.env # Gemini CLI → hush
90
+ └── opencode.json # OpenCode → hush
91
+ ```
92
+
93
+ **Claude Code** — `.claude/settings.json`:
94
+ ```json
95
+ {
96
+ "env": {
97
+ "ANTHROPIC_BASE_URL": "http://127.0.0.1:4000"
98
+ }
99
+ }
100
+ ```
101
+
102
+ **Codex** — `.codex/config.toml`:
103
+ ```toml
104
+ model_provider = "hush"
105
+
106
+ [model_providers.hush]
107
+ base_url = "http://127.0.0.1:4000/v1"
108
+ ```
109
+
110
+ **OpenCode** — `opencode.json`:
111
+ ```json
112
+ {
113
+ "provider": {
114
+ "zai-coding-plan": {
115
+ "options": {
116
+ "baseURL": "http://127.0.0.1:4000/api/coding/paas/v4"
117
+ }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ **Gemini CLI** — `.gemini/.env`:
124
+ ```
125
+ CODE_ASSIST_ENDPOINT=http://127.0.0.1:4000
126
+ ```
127
+
128
+ Each developer just needs `hush` running locally. All AI tools in the project will route through it automatically.
129
+
130
+ ## Hooks Mode (Claude Code)
131
+
132
+ Hush can also run as a **Claude Code hook** — redacting PII from tool outputs *before Claude ever sees them*. No proxy required.
133
+
134
+ ### Setup
135
+
136
+ ```bash
137
+ hush init --hooks
138
+ ```
139
+
140
+ This adds a `PostToolUse` hook to `.claude/settings.json` that runs `hush redact-hook` after every `Bash`, `Read`, `Grep`, and `WebFetch` tool call.
141
+
142
+ Use `--local` to write to `settings.local.json` instead (for personal overrides not committed to the repo).
143
+
144
+ ### How it works
145
+
146
+ ```
147
+ Local files/commands → [Hook: redact before Claude sees] → Claude's context
148
+
149
+ API request
150
+
151
+ [Proxy: redact before cloud]
152
+
153
+ LLM Provider
154
+ ```
155
+
156
+ When a tool runs (e.g., `cat .env`), the hook inspects the response for PII. If PII is found, the hook **blocks** the raw output and provides Claude with the redacted version instead. Claude only ever sees `[USER_EMAIL_f22c5a]`, not `alice@company.com`.
157
+
158
+ ### Hooks vs Proxy
159
+
160
+ | | Hooks Mode | Proxy Mode |
161
+ |---|---|---|
162
+ | **What's protected** | Tool outputs (before Claude sees them) | API requests (before they leave your machine) |
163
+ | **Setup** | `hush init --hooks` | `hush` + point `ANTHROPIC_BASE_URL` |
164
+ | **Works with** | Claude Code only | Any AI tool |
165
+ | **Defense-in-depth** | Use both for maximum coverage | Use both for maximum coverage |
166
+
167
+ ### Defense-in-depth
168
+
169
+ For maximum protection, use both modes together. The team config example in [`examples/team-config/`](examples/team-config/) shows this setup — hooks redact tool outputs and the proxy redacts API requests.
170
+
171
+ ## OpenCode Plugin
172
+
173
+ Hush provides an **OpenCode plugin** that blocks reads of sensitive files (`.env`, `*.pem`, `credentials.*`, `id_rsa`, etc.) before the tool executes — the AI model never sees the contents.
174
+
175
+ ### Drop-in setup
176
+
177
+ Copy the plugin file and update your `opencode.json`:
178
+
179
+ ```
180
+ your-project/
181
+ ├── .opencode/plugins/hush.ts # plugin file
182
+ └── opencode.json # add "plugin" array
183
+ ```
184
+
185
+ ```json
186
+ {
187
+ "provider": {
188
+ "zai-coding-plan": {
189
+ "options": {
190
+ "baseURL": "http://127.0.0.1:4000/api/coding/paas/v4"
191
+ }
192
+ }
193
+ },
194
+ "plugin": [".opencode/plugins/hush.ts"]
195
+ }
196
+ ```
197
+
198
+ Find the drop-in plugin at [`examples/team-config/.opencode/plugins/hush.ts`](examples/team-config/.opencode/plugins/hush.ts).
199
+
200
+ ### npm import
201
+
202
+ ```typescript
203
+ import { HushPlugin } from '@aictrl/hush/opencode-plugin'
204
+ ```
205
+
206
+ ### What it blocks
207
+
208
+ | Tool | Blocked when |
209
+ |------|-------------|
210
+ | `read` | File path matches `.env*`, `*credentials*`, `*secret*`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `*.asc`, `id_rsa*`, `.netrc`, `.pgpass` |
211
+ | `bash` | Commands like `cat`, `head`, `tail`, `less`, `more`, `bat` target a sensitive file |
212
+
213
+ ### Plugin + Proxy = Defense-in-depth
214
+
215
+ The plugin blocks reads of known-sensitive filenames. The proxy catches PII in files with normal names (e.g., `config.txt` containing an email). Together they provide two layers of protection:
216
+
217
+ ```
218
+ Tool reads .env → [Plugin: BLOCKED] → model never sees it
219
+ Tool reads config.txt → [Plugin: allowed] → proxy redacts PII → model sees tokens
220
+ (not a sensitive filename)
221
+ ```
222
+
76
223
  ## How it Works
77
224
 
78
225
  1. **Intercept** — Hush sits on your machine between your AI tool and the LLM provider.
@@ -88,7 +235,7 @@ Your tool still sees the real data (rehydrated locally). The LLM provider only e
88
235
  | Claude Code | `~/.claude/settings.json` | `/v1/messages` → Anthropic |
89
236
  | Codex | `~/.codex/config.toml` | `/v1/chat/completions` → OpenAI |
90
237
  | OpenCode | `opencode.json` | `/api/paas/v4/**` → ZhipuAI |
91
- | Gemini CLI | `CODE_ASSIST_ENDPOINT` env var | `/v1beta/models/**` → Google |
238
+ | Gemini CLI | `.gemini/.env` | `/v1beta/models/**` → Google |
92
239
  | Any tool | Point base URL at hush | `/*` catch-all with auto-detect |
93
240
 
94
241
  Hush forwards your existing auth headers transparently — no API keys need to be reconfigured.
package/dist/cli.js CHANGED
@@ -1,19 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import { app } from './index.js';
3
- import { createLogger } from './lib/logger.js';
4
- const log = createLogger('hush-cli');
5
- const PORT = process.env.PORT || 4000;
6
- const server = app.listen(PORT, () => {
7
- log.info(`Hush Semantic Gateway is listening on http://localhost:${PORT}`);
8
- log.info(`Routes: /v1/messages Anthropic, /v1/chat/completions OpenAI, /api/paas/v4/** → ZhipuAI, * → Google`);
9
- });
10
- server.on('error', (err) => {
11
- if (err.code === 'EADDRINUSE') {
12
- log.error(`Port ${PORT} is already in use. Stop the other process or use PORT=<number> hush`);
13
- }
14
- else {
15
- log.error({ err }, 'Failed to start server');
16
- }
17
- process.exit(1);
18
- });
2
+ const subcommand = process.argv[2];
3
+ if (subcommand === 'redact-hook') {
4
+ const { run } = await import('./commands/redact-hook.js');
5
+ await run();
6
+ }
7
+ else if (subcommand === 'init') {
8
+ const { run } = await import('./commands/init.js');
9
+ run(process.argv.slice(3));
10
+ }
11
+ else {
12
+ // Default: start the proxy server
13
+ const { app } = await import('./index.js');
14
+ const { createLogger } = await import('./lib/logger.js');
15
+ const log = createLogger('hush-cli');
16
+ const PORT = process.env.PORT || 4000;
17
+ const server = app.listen(PORT, () => {
18
+ log.info(`Hush Semantic Gateway is listening on http://localhost:${PORT}`);
19
+ log.info(`Routes: /v1/messages → Anthropic, /v1/chat/completions → OpenAI, /api/paas/v4/** → ZhipuAI, * → Google`);
20
+ });
21
+ server.on('error', (err) => {
22
+ if (err.code === 'EADDRINUSE') {
23
+ log.error(`Port ${PORT} is already in use. Stop the other process or use PORT=<number> hush`);
24
+ }
25
+ else {
26
+ log.error({ err }, 'Failed to start server');
27
+ }
28
+ process.exit(1);
29
+ });
30
+ }
31
+ export {};
19
32
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;AACrC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACnC,GAAG,CAAC,IAAI,CAAC,0DAA0D,IAAI,EAAE,CAAC,CAAC;IAC3E,GAAG,CAAC,IAAI,CAAC,wGAAwG,CAAC,CAAC;AACrH,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;IAChD,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC9B,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,sEAAsE,CAAC,CAAC;IAChG,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,wBAAwB,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,UAAU,KAAK,aAAa,EAAE,CAAC;IACjC,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,2BAA2B,CAAC,CAAC;IAC1D,MAAM,GAAG,EAAE,CAAC;AACd,CAAC;KAAM,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;IACjC,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;IACnD,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC;KAAM,CAAC;IACN,kCAAkC;IAClC,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;IAC3C,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;IAEzD,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;IAEtC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QACnC,GAAG,CAAC,IAAI,CAAC,0DAA0D,IAAI,EAAE,CAAC,CAAC;QAC3E,GAAG,CAAC,IAAI,CAAC,wGAAwG,CAAC,CAAC;IACrH,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;QAChD,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC9B,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,sEAAsE,CAAC,CAAC;QAChG,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,wBAAwB,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * hush init — Generate hook configuration for Claude Code or Gemini CLI
3
+ *
4
+ * Usage:
5
+ * hush init --hooks Write to .claude/settings.json
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
9
+ */
10
+ export declare function run(args: string[]): void;
11
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiIH,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAgDxC"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * hush init — Generate hook configuration for Claude Code or Gemini CLI
3
+ *
4
+ * Usage:
5
+ * hush init --hooks Write to .claude/settings.json
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
9
+ */
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
11
+ import { join } from 'path';
12
+ const HUSH_HOOK = {
13
+ type: 'command',
14
+ command: 'hush redact-hook',
15
+ timeout: 10,
16
+ };
17
+ const CLAUDE_HOOK_CONFIG = {
18
+ hooks: {
19
+ PreToolUse: [
20
+ {
21
+ matcher: 'mcp__.*',
22
+ hooks: [HUSH_HOOK],
23
+ },
24
+ ],
25
+ PostToolUse: [
26
+ {
27
+ matcher: 'Bash|Read|Grep|WebFetch',
28
+ hooks: [HUSH_HOOK],
29
+ },
30
+ {
31
+ matcher: 'mcp__.*',
32
+ hooks: [HUSH_HOOK],
33
+ },
34
+ ],
35
+ },
36
+ };
37
+ const GEMINI_HOOK_CONFIG = {
38
+ hooks: {
39
+ BeforeTool: [
40
+ {
41
+ matcher: 'mcp__.*',
42
+ hooks: [HUSH_HOOK],
43
+ },
44
+ ],
45
+ AfterTool: [
46
+ {
47
+ matcher: 'run_shell_command|read_file|read_many_files|search_file_content|web_fetch',
48
+ hooks: [HUSH_HOOK],
49
+ },
50
+ {
51
+ matcher: 'mcp__.*',
52
+ hooks: [HUSH_HOOK],
53
+ },
54
+ ],
55
+ },
56
+ };
57
+ function hasHushHookInEntries(entries) {
58
+ if (!Array.isArray(entries))
59
+ return false;
60
+ return entries.some((entry) => entry.hooks?.some((h) => h.command?.includes('hush redact-hook')));
61
+ }
62
+ function hasHushHookClaude(settings) {
63
+ return (hasHushHookInEntries(settings.hooks?.PreToolUse) &&
64
+ hasHushHookInEntries(settings.hooks?.PostToolUse));
65
+ }
66
+ function hasHushHookGemini(settings) {
67
+ return (hasHushHookInEntries(settings.hooks?.BeforeTool) &&
68
+ hasHushHookInEntries(settings.hooks?.AfterTool));
69
+ }
70
+ function mergeHookEntries(existing, newEntries) {
71
+ const merged = Array.isArray(existing) ? [...existing] : [];
72
+ for (const entry of newEntries) {
73
+ const alreadyHas = merged.some((e) => e.matcher === entry.matcher &&
74
+ e.hooks?.some((h) => h.command?.includes('hush redact-hook')));
75
+ if (!alreadyHas) {
76
+ merged.push(entry);
77
+ }
78
+ }
79
+ return merged;
80
+ }
81
+ function mergeHooks(existing, hookConfig) {
82
+ const merged = { ...existing };
83
+ if (!merged.hooks) {
84
+ merged.hooks = {};
85
+ }
86
+ for (const [eventName, entries] of Object.entries(hookConfig.hooks)) {
87
+ const existingEntries = merged.hooks[eventName];
88
+ merged.hooks[eventName] = mergeHookEntries(existingEntries, entries);
89
+ }
90
+ return merged;
91
+ }
92
+ export function run(args) {
93
+ const hasHooksFlag = args.includes('--hooks');
94
+ const isLocal = args.includes('--local');
95
+ const isGemini = args.includes('--gemini');
96
+ if (!hasHooksFlag) {
97
+ process.stderr.write('Usage: hush init --hooks [--local] [--gemini]\n');
98
+ process.stderr.write('\n');
99
+ process.stderr.write('Options:\n');
100
+ process.stderr.write(' --hooks Generate hook config (PreToolUse + PostToolUse or BeforeTool + AfterTool)\n');
101
+ process.stderr.write(' --local Write to settings.local.json instead of settings.json\n');
102
+ process.stderr.write(' --gemini Write Gemini CLI hooks instead of Claude Code hooks\n');
103
+ process.exit(1);
104
+ }
105
+ const dirName = isGemini ? '.gemini' : '.claude';
106
+ const configDir = join(process.cwd(), dirName);
107
+ const filename = isLocal ? 'settings.local.json' : 'settings.json';
108
+ const filePath = join(configDir, filename);
109
+ // Ensure config dir exists
110
+ if (!existsSync(configDir)) {
111
+ mkdirSync(configDir, { recursive: true });
112
+ }
113
+ // Read existing settings or start fresh
114
+ let settings = {};
115
+ if (existsSync(filePath)) {
116
+ try {
117
+ const raw = readFileSync(filePath, 'utf-8');
118
+ settings = JSON.parse(raw);
119
+ }
120
+ catch {
121
+ process.stderr.write(`Warning: could not parse ${filePath}, starting fresh\n`);
122
+ }
123
+ }
124
+ // Idempotency check
125
+ const hookConfig = isGemini ? GEMINI_HOOK_CONFIG : CLAUDE_HOOK_CONFIG;
126
+ const hasHook = isGemini ? hasHushHookGemini : hasHushHookClaude;
127
+ if (hasHook(settings)) {
128
+ process.stdout.write(`hush hooks already configured in ${filePath}\n`);
129
+ return;
130
+ }
131
+ const merged = mergeHooks(settings, hookConfig);
132
+ writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n');
133
+ process.stdout.write(`Wrote hush hooks config to ${filePath}\n`);
134
+ }
135
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * hush redact-hook — Hook handler for Claude Code and Gemini CLI
3
+ *
4
+ * Reads the hook payload from stdin, redacts PII, and returns the
5
+ * appropriate response format depending on the hook event type:
6
+ *
7
+ * Claude Code:
8
+ * PreToolUse — redacts outbound MCP tool arguments (updatedInput)
9
+ * PostToolUse — redacts inbound MCP tool results (updatedMCPToolOutput)
10
+ * or blocks built-in tool output (decision: "block")
11
+ *
12
+ * Gemini CLI:
13
+ * BeforeTool — redacts outbound MCP tool arguments (hookSpecificOutput.tool_input)
14
+ * AfterTool — redacts inbound tool results (decision: "deny")
15
+ *
16
+ * Exit codes:
17
+ * 0 — success (may or may not redact)
18
+ * 2 — malformed input (blocks the tool call per hooks spec)
19
+ */
20
+ export declare function run(): Promise<void>;
21
+ //# sourceMappingURL=redact-hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact-hook.d.ts","sourceRoot":"","sources":["../../src/commands/redact-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA8NH,wBAAsB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAwDzC"}