@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.
- package/.github/workflows/opencode-review.yml +52 -7
- package/.gitlab-ci.yml +59 -0
- package/README.md +150 -3
- package/dist/cli.js +30 -17
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +135 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/redact-hook.d.ts +21 -0
- package/dist/commands/redact-hook.d.ts.map +1 -0
- package/dist/commands/redact-hook.js +225 -0
- package/dist/commands/redact-hook.js.map +1 -0
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/redactor.d.ts +5 -0
- package/dist/middleware/redactor.d.ts.map +1 -1
- package/dist/middleware/redactor.js +69 -0
- package/dist/middleware/redactor.js.map +1 -1
- package/dist/plugins/opencode-hush.d.ts +32 -0
- package/dist/plugins/opencode-hush.d.ts.map +1 -0
- package/dist/plugins/opencode-hush.js +58 -0
- package/dist/plugins/opencode-hush.js.map +1 -0
- package/dist/plugins/sensitive-patterns.d.ts +15 -0
- package/dist/plugins/sensitive-patterns.d.ts.map +1 -0
- package/dist/plugins/sensitive-patterns.js +69 -0
- package/dist/plugins/sensitive-patterns.js.map +1 -0
- package/dist/vault/token-vault.d.ts.map +1 -1
- package/dist/vault/token-vault.js +16 -3
- package/dist/vault/token-vault.js.map +1 -1
- package/examples/team-config/.claude/settings.json +41 -0
- package/examples/team-config/.codex/config.toml +4 -0
- package/examples/team-config/.gemini/settings.json +38 -0
- package/examples/team-config/.opencode/plugins/hush.ts +79 -0
- package/examples/team-config/opencode.json +10 -0
- package/package.json +11 -1
- package/scripts/e2e-plugin-block.sh +142 -0
- package/scripts/e2e-proxy-live.sh +185 -0
- package/src/cli.ts +28 -16
- package/src/commands/init.ts +186 -0
- package/src/commands/redact-hook.ts +297 -0
- package/src/index.ts +7 -2
- package/src/middleware/redactor.ts +75 -0
- package/src/plugins/opencode-hush.ts +70 -0
- package/src/plugins/sensitive-patterns.ts +71 -0
- package/src/vault/token-vault.ts +18 -4
- package/tests/init.test.ts +255 -0
- package/tests/opencode-plugin.test.ts +219 -0
- package/tests/redact-hook.test.ts +498 -0
- 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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
60
|
+
Add `.gemini/.env` to your project root (or set the env var directly):
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
|
|
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 | `
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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":";
|
|
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"}
|