@etiennepasteur/jean-claude 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/dist/cli.mjs +1631 -0
  4. package/package.json +69 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Etienne Pasteur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,422 @@
1
+ # jean-claude
2
+
3
+ A CLI MITM HTTPS proxy that intercepts a tool's API traffic and rewrites it from a
4
+ YAML file. The tool being intercepted never knows.
5
+
6
+ It was built for one job in particular: **freezing the settings Claude Code
7
+ fetches at startup**, so the ones you control apply instead of the ones the API
8
+ hands back. Everything else it does — capture, stub, patch, redirect — falls out
9
+ of the same machinery and works against any HTTPS client.
10
+
11
+ ```
12
+ Claude Code ──► jean-claude ──► api.anthropic.com
13
+
14
+ └── ~/.config/jean-claude/jean-claude.yaml
15
+ + responses/settings.GET.json
16
+ ```
17
+
18
+ ## Freezing Claude Code's settings
19
+
20
+ On startup, Claude Code calls `GET https://api.anthropic.com/api/claude_code/settings`
21
+ and applies the managed settings attached to your account. The response looks like
22
+ this:
23
+
24
+ ```json
25
+ {
26
+ "uuid": "…",
27
+ "checksum": "sha256:…",
28
+ "settings": {
29
+ "env": { "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" },
30
+ "permissions": { "defaultMode": "plan" }
31
+ }
32
+ }
33
+ ```
34
+
35
+ jean-claude answers that one request from a local file. It never leaves the
36
+ machine, and the settings stop moving under you between sessions. Every other call
37
+ Claude Code makes — the actual conversation — is relayed untouched.
38
+
39
+ ### 1. Set it up
40
+
41
+ ```bash
42
+ npm install -g @etiennepasteur/jean-claude
43
+ jean-claude init --claude-code
44
+ ```
45
+
46
+ The package is scoped, but the command it installs is plain `jean-claude` — that
47
+ is what every example below uses.
48
+
49
+ Or without installing anything — `npx` works the same way:
50
+
51
+ ```bash
52
+ npx @etiennepasteur/jean-claude init --claude-code
53
+ npx @etiennepasteur/jean-claude run -- claude
54
+ ```
55
+
56
+ Installing globally is the better default for something you launch every session:
57
+ the dependency tree is ~55 MB, so the first `npx` run is a real download, and even
58
+ warm it adds about half a second of resolution. `npx` is the right call for trying
59
+ it once, or for a throwaway CI step.
60
+
61
+ Either way, that writes `~/.config/jean-claude/` with a CA, a starter stub, and
62
+ this config:
63
+
64
+ ```yaml
65
+ host: api.anthropic.com
66
+
67
+ rules:
68
+ - name: frozen claude_code settings
69
+ method: GET
70
+ path: /api/claude_code/settings
71
+ respond: ./responses/settings.GET.json
72
+ ```
73
+
74
+ ### 2. Edit the settings
75
+
76
+ Open `~/.config/jean-claude/responses/settings.GET.json` and put what you want
77
+ in the `settings` object. The file is re-read on **every request**, so a change
78
+ applies the next time Claude Code starts — no proxy restart, no reload.
79
+
80
+ To start from what your account actually sends rather than from the scaffolded
81
+ stub, record it once:
82
+
83
+ ```bash
84
+ jean-claude run --record ./captures -- claude
85
+ cp ./captures/api.anthropic.com/api/claude_code/settings.GET.json \
86
+ ~/.config/jean-claude/responses/settings.GET.json
87
+ ```
88
+
89
+ ### 3. Run it
90
+
91
+ ```bash
92
+ jean-claude run -- claude
93
+ ```
94
+
95
+ Add `-v` the first time, to see every request rather than only the ones a rule
96
+ touched:
97
+
98
+ ```
99
+ GET api.anthropic.com/api/claude_code/settings 200 → stub responses/settings.GET.json
100
+ POST api.anthropic.com/v1/messages 200
101
+ ```
102
+
103
+ A `→ stub` on the settings line means it worked. If you would rather keep the log
104
+ out of Claude Code's output, run the proxy in its own terminal —
105
+ see [Two terminals](#two-terminals-start--env).
106
+
107
+ ## Where everything lives
108
+
109
+ ```
110
+ ~/.config/jean-claude/ # $XDG_CONFIG_HOME/jean-claude if set
111
+ ├── jean-claude.yaml
112
+ ├── responses/
113
+ ├── ca/ ca.pem ca.key bundle.pem
114
+ └── session.json # only while `start` is running
115
+ ```
116
+
117
+ The config is resolved in this order:
118
+
119
+ 1. `--config <path>`
120
+ 2. `jean-claude.yaml` (or `.yml`) in the current directory, then walking up
121
+ 3. `~/.config/jean-claude/jean-claude.yaml`
122
+
123
+ So the global rule applies wherever you launch Claude Code from, and a repo that
124
+ drops its own `jean-claude.yaml` at the root overrides it for that project. Paths
125
+ inside a config (`respond:`, `patch: { file: }`) are always relative to that
126
+ config file.
127
+
128
+ `--home <dir>` relocates the whole directory — for a throwaway setup, or to run
129
+ two proxies side by side.
130
+
131
+ ## Beyond Claude Code
132
+
133
+ Nothing in the machinery is Anthropic-specific. `jean-claude init` without
134
+ `--claude-code` scaffolds a generic config, and the same rules work against any
135
+ HTTPS client that honours the proxy environment variables:
136
+
137
+ ```bash
138
+ jean-claude init
139
+ jean-claude run -- npx your-tool
140
+ ```
141
+
142
+ The problem it solves in general: you are developing against a third-party API and
143
+ you need to _see_ and _falsify_ what a tool receives, without touching either the
144
+ tool or the server. Reproduce a 500. Test a business case that is absent from the
145
+ dataset. Freeze a flaky response. Or simply find out what the tool actually calls.
146
+
147
+ ### 1. Replace the response with a file
148
+
149
+ The request never leaves the machine. This is what the Claude Code rule uses.
150
+
151
+ ```yaml
152
+ rules:
153
+ - path: /api/todos
154
+ method: GET
155
+ respond: ./responses/todos.json
156
+ ```
157
+
158
+ The stub file is re-read on **every request**, so editing it takes effect
159
+ immediately — no restart, no reload.
160
+
161
+ Longer form, with status, headers and latency:
162
+
163
+ ```yaml
164
+ rules:
165
+ - path: /api/todos
166
+ respond:
167
+ file: ./responses/todos.json
168
+ status: 201
169
+ headers: { x-jean-claude: stub }
170
+ delay: 500
171
+ ```
172
+
173
+ Or an inline body, with no file at all:
174
+
175
+ ```yaml
176
+ rules:
177
+ - path: /api/health
178
+ respond:
179
+ body: { status: 'ok' }
180
+ ```
181
+
182
+ ### 2. Patch the real response
183
+
184
+ The request _does_ reach the server; jean-claude edits what comes back. Useful
185
+ when you want the live payload with one field bent, rather than a frozen copy
186
+ that goes stale.
187
+
188
+ ```yaml
189
+ rules:
190
+ # Precise edit, and the only option that works on an array response.
191
+ - path: /api/todos
192
+ patch:
193
+ jsonPatch: [{ op: replace, path: /0/title, value: Dining }]
194
+
195
+ # Deep merge, for object responses.
196
+ - path: /api/users/:id
197
+ patch:
198
+ merge: { verified: true }
199
+
200
+ # Simulate a failing, slow server. The original body is preserved
201
+ # unless you replace it.
202
+ - path: /api/flaky
203
+ patch:
204
+ status: 500
205
+ body: { error: 'boom' }
206
+ delay: 2000
207
+ ```
208
+
209
+ `patch` accepts `status`, `headers` (merged), `replaceHeaders` (wholesale),
210
+ `merge`, `jsonPatch`, `body`, `file` and `delay`.
211
+
212
+ ### 3. Rewrite the outgoing request
213
+
214
+ ```yaml
215
+ rules:
216
+ - host: auth.example.com
217
+ path: /oauth/token
218
+ request:
219
+ host: staging-auth.example.com
220
+ path: /v2/token
221
+ headers: { authorization: 'Bearer TEST' }
222
+ removeHeaders: [cookie]
223
+ ```
224
+
225
+ `request` combines with `patch` — you can redirect a call _and_ doctor its
226
+ response. It cannot combine with `respond`, which short circuits the request
227
+ before it goes anywhere.
228
+
229
+ ## Matching
230
+
231
+ ```yaml
232
+ host: api.example.com # default host for every rule below
233
+ port: 8888 # optional, otherwise a free port is picked
234
+
235
+ rules:
236
+ - host: auth.example.com # overrides the default
237
+ method: [POST, PUT] # one method or a list, case-insensitive
238
+ path: /api/users/:id # exact, :param, or a bare * wildcard
239
+ query: { page: '2' } # required params; extra ones are ignored
240
+ ```
241
+
242
+ - A rule with no `host` matches **any** host.
243
+ - `*.example.com` matches any subdomain.
244
+ - `path` is compiled by `path-to-regexp`: `/api/todos`, `/api/users/:id`, `/api/*`.
245
+ Use `pathRegex` when you need a raw regular expression.
246
+ - Captured parameters interpolate into the stub path:
247
+ `respond: ./responses/users/{id}.json`.
248
+ - Rules are tried in file order and **the first match wins**. `jean-claude check`
249
+ prints them as resolved.
250
+ - Anything matching no rule is still decrypted, logged and relayed untouched.
251
+
252
+ ## Discovering and capturing traffic
253
+
254
+ ```bash
255
+ jean-claude run --record ./captures -- npx your-tool
256
+ ```
257
+
258
+ ```
259
+ GET api.example.com/api/todos 200 → stub responses/todos.json
260
+ GET api.example.com/api/users/42 200 ⇒ captures/api.example.com/api/users/42.GET.json
261
+ POST api.example.com/api/login 401 ⇒ captures/api.example.com/api/login.POST.json
262
+ ```
263
+
264
+ Captures contain the **body only**, re-indented, so a captured file drops
265
+ straight into a `respond:` rule — that is the shortcut used to seed the Claude
266
+ Code stub above. When a rule patches a response, the capture is still the
267
+ untouched server response, not what the client saw.
268
+
269
+ By default only affected traffic is logged. Add `-v` to see everything, `-q` to
270
+ see nothing.
271
+
272
+ ## Certificates
273
+
274
+ On first run, jean-claude generates a CA in `~/.config/jean-claude/ca/`:
275
+
276
+ | File | Role |
277
+ | ------------------- | ------------------------------------------------------------------------------ |
278
+ | `ca.pem` / `ca.key` | the authority that signs per-host certificates on the fly (`ca.key` is `0600`) |
279
+ | `bundle.pem` | `ca.pem` + the system trust store + any inherited corporate CA |
280
+
281
+ The bundle exists because `SSL_CERT_FILE` and `CURL_CA_BUNDLE` **replace** the
282
+ trust store rather than adding to it — handing a target only our own CA would cut
283
+ it off from every other authority.
284
+
285
+ `jean-claude run` points the child at the bundle, so **no root access and no
286
+ system-wide trust change is needed**. If you do want the CA in the system store
287
+ (for a GUI app, say), `jean-claude ca --install` prints the commands for you to
288
+ run yourself. jean-claude never invokes `sudo` on its own.
289
+
290
+ ### Behind a corporate proxy
291
+
292
+ jean-claude reads the environment before rewriting it for the child, and chains
293
+ onto whatever it finds:
294
+
295
+ - `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` → relayed traffic goes out through
296
+ your corporate proxy. Override with `upstream:` in the config (`auto`, `off`,
297
+ or an explicit URL).
298
+ - `NODE_EXTRA_CA_CERTS` → your corporate CA is trusted on outbound connections
299
+ _and_ folded into `bundle.pem` for the child.
300
+
301
+ ### When a target refuses the CA
302
+
303
+ A client that pins its certificates will fail the handshake. jean-claude says so
304
+ explicitly rather than leaving you guessing, and the fix is to tunnel that host
305
+ without interception:
306
+
307
+ ```yaml
308
+ tlsPassthrough:
309
+ - pinned.example.com
310
+ ```
311
+
312
+ ## How the target is pointed at the proxy
313
+
314
+ `jean-claude run` injects these into the child environment:
315
+
316
+ | Variable | Why |
317
+ | ------------------------------------------------------------------------------------------ | --------------------------------------------------- |
318
+ | `HTTP_PROXY`, `HTTPS_PROXY` (+ lowercase) | the universal convention |
319
+ | `NODE_USE_ENV_PROXY=1` | **required**: Node ignores `HTTPS_PROXY` without it |
320
+ | `NODE_EXTRA_CA_CERTS` | Node targets |
321
+ | `SSL_CERT_FILE`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `AWS_CA_BUNDLE`, `GIT_SSL_CAINFO` | OpenSSL, curl, python/requests, AWS CLI, git |
322
+
323
+ `NO_PROXY` is **unset**, not narrowed. The tempting default is
324
+ `localhost,127.0.0.1,::1`, but that makes every localhost target bypass
325
+ jean-claude silently — and intercepting a local dev API is one of the main reasons
326
+ to reach for this tool. A bypass that still produces plausible traffic is the
327
+ worst possible failure mode, so exclusions are opt-in:
328
+
329
+ ```yaml
330
+ noProxy:
331
+ - metrics.internal
332
+ ```
333
+
334
+ Exclusions are printed in the startup banner, because they are the one setting
335
+ that makes traffic invisible.
336
+
337
+ `NODE_USE_ENV_PROXY` only exists in Node **≥ 22.21** or **≥ 24.5**. Below that, a
338
+ Node-based target silently ignores the proxy; jean-claude warns you at startup.
339
+ Claude Code ships as a Node CLI, so this applies to it too.
340
+
341
+ Known gaps worth knowing rather than fighting: Java uses its own keystore
342
+ (`keytool`), Rust tools built on `rustls` embed their roots and ignore every
343
+ environment variable, and anything with pinned certificates needs
344
+ `tlsPassthrough`.
345
+
346
+ ## Two terminals: `start` + `env`
347
+
348
+ For a GUI app, a service that is already running, or simply to keep jean-claude's
349
+ log out of your tool's output:
350
+
351
+ ```bash
352
+ # terminal 1 - the proxy and its log live here
353
+ jean-claude start -v
354
+
355
+ # terminal 2 - your tool
356
+ eval "$(jean-claude env)"
357
+ claude
358
+ ```
359
+
360
+ `start` records the live session (port, bundle path, pid) in
361
+ `~/.config/jean-claude/session.json`, and `env` reads it — so the second shell
362
+ needs no arguments and no copy-pasting. The file is removed when `start` exits,
363
+ and `env` refuses a stale one rather than handing out a dead port.
364
+
365
+ Note that `eval "$(jean-claude start --export)"` **cannot** work: `start` runs in
366
+ the foreground, so the command substitution would never return. That is exactly
367
+ why `env` exists.
368
+
369
+ Use `jean-claude env -p 8899` to target a session started elsewhere, `--json` for
370
+ machine-readable output. One session is recorded per home; with several
371
+ concurrent proxies, pass `-p` (or a separate `--home`).
372
+
373
+ ## Commands
374
+
375
+ ```
376
+ jean-claude run -- <command> run a command with its HTTPS traffic intercepted
377
+ jean-claude start run the proxy alone, in its own terminal
378
+ jean-claude env print the environment for a running `start`
379
+ jean-claude check validate the config, print the rules as resolved
380
+ jean-claude ca show the certificate store, and how to trust it
381
+ jean-claude init set up the jean-claude directory: config, stub, CA
382
+ ```
383
+
384
+ Shared flags: `-c/--config`, `-p/--port`, `-r/--record`, `--home`,
385
+ `-v/--verbose`, `-q/--quiet`, `--no-watch`.
386
+
387
+ `init` takes `--home <dir>` and `--claude-code`.
388
+
389
+ `run` exits with the child's exit code, so it drops into a CI pipeline unchanged.
390
+
391
+ The config file is watched and reloaded on save. A config that fails to parse is
392
+ reported and the previous rules stay in effect. Changing `tlsPassthrough` is the
393
+ one setting that needs a restart.
394
+
395
+ ## Development
396
+
397
+ ```bash
398
+ npm install
399
+ npm test # 130 tests, including an end-to-end MITM suite
400
+ npm run typecheck
401
+ npm run lint
402
+ npm run build
403
+ npm run jc -- --help # run from source
404
+ ```
405
+
406
+ The end-to-end suite runs a plain `node:https` server as the fake upstream,
407
+ holding a leaf certificate minted by jean-claude's own CA, so it needs no network
408
+ access. It asserts the things that actually matter: that a `respond` rule never
409
+ reaches the server, that `patch` does, and that a status-only patch leaves the
410
+ body intact.
411
+
412
+ One gotcha worth knowing if you extend the suite: pointing jean-claude at a
413
+ _second mockttp instance_ stalls for ~15s on the first upstream connection. It is
414
+ an artefact of mockttp talking to mockttp, not of jean-claude — against a real
415
+ HTTPS server the relay costs ~25ms. Hence the plain `node:https` upstream.
416
+
417
+ Built on [mockttp](https://github.com/httptoolkit/mockttp), which does the heavy
418
+ lifting: CONNECT tunnelling, per-host certificate minting, HTTP/2 and WebSockets.
419
+
420
+ ## Licence
421
+
422
+ MIT — see [LICENSE](LICENSE).