claude-cac 1.3.1 → 1.3.3
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/README.md +6 -2
- package/cac +192 -184
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/logo-dark.svg">
|
|
5
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/images/logo-light.svg">
|
|
6
|
+
<img alt="cac" src="docs/images/logo-light.svg" width="200">
|
|
7
|
+
</picture>
|
|
4
8
|
|
|
5
|
-
**Claude Code
|
|
9
|
+
**Claude Code 小雨衣** — Isolate, protect, and manage your Claude Code.
|
|
6
10
|
|
|
7
11
|
*Run Claude Code your way — isolated, protected, managed.*
|
|
8
12
|
|
package/cac
CHANGED
|
@@ -8,10 +8,10 @@ ENVS_DIR="$CAC_DIR/envs"
|
|
|
8
8
|
VERSIONS_DIR="$CAC_DIR/versions"
|
|
9
9
|
|
|
10
10
|
# ━━━ utils.sh ━━━
|
|
11
|
-
# ── utils:
|
|
11
|
+
# ── utils: colors, read/write, UUID, proxy parsing ───────────────────────
|
|
12
12
|
|
|
13
13
|
# shellcheck disable=SC2034 # used in build-concatenated cac script
|
|
14
|
-
CAC_VERSION="1.3.
|
|
14
|
+
CAC_VERSION="1.3.3"
|
|
15
15
|
|
|
16
16
|
_read() { [[ -f "$1" ]] && tr -d '[:space:]' < "$1" || echo "${2:-}"; }
|
|
17
17
|
_die() { printf '%b\n' "$(_red "error:") $*" >&2; exit 1; }
|
|
@@ -48,7 +48,7 @@ _new_machine_id() { _gen_uuid | tr -d '-' | tr '[:upper:]' '[:lower:]'; }
|
|
|
48
48
|
_new_hostname() { echo "host-$(_gen_uuid | cut -d- -f1 | tr '[:upper:]' '[:lower:]')"; }
|
|
49
49
|
_new_mac() { printf '02:%02x:%02x:%02x:%02x:%02x' $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)); }
|
|
50
50
|
|
|
51
|
-
#
|
|
51
|
+
# Get real command path (bypass shim)
|
|
52
52
|
_get_real_cmd() {
|
|
53
53
|
local cmd="$1"
|
|
54
54
|
PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
|
|
@@ -56,15 +56,15 @@ _get_real_cmd() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
# host:port:user:pass → http://user:pass@host:port
|
|
59
|
-
#
|
|
59
|
+
# or pass a full URL directly (http://, https://, socks5://)
|
|
60
60
|
_parse_proxy() {
|
|
61
61
|
local raw="$1"
|
|
62
|
-
#
|
|
62
|
+
# Already a full URL, return as-is
|
|
63
63
|
if [[ "$raw" =~ ^(http|https|socks5):// ]]; then
|
|
64
64
|
echo "$raw"
|
|
65
65
|
return
|
|
66
66
|
fi
|
|
67
|
-
#
|
|
67
|
+
# Parse host:port:user:pass format
|
|
68
68
|
local host port user pass
|
|
69
69
|
host=$(echo "$raw" | cut -d: -f1)
|
|
70
70
|
port=$(echo "$raw" | cut -d: -f2)
|
|
@@ -90,11 +90,11 @@ _proxy_reachable() {
|
|
|
90
90
|
(echo >/dev/tcp/"$host"/"$port") 2>/dev/null
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
93
|
+
# Auto-detect proxy protocol (when user didn't specify http/socks5/https)
|
|
94
|
+
# Usage: _auto_detect_proxy "host:port:user:pass" → returns a working full URL
|
|
95
95
|
_auto_detect_proxy() {
|
|
96
96
|
local raw="$1"
|
|
97
|
-
#
|
|
97
|
+
# Has protocol prefix, return as-is
|
|
98
98
|
if [[ "$raw" =~ ^(http|https|socks5):// ]]; then
|
|
99
99
|
echo "$raw"
|
|
100
100
|
return 0
|
|
@@ -111,7 +111,7 @@ _auto_detect_proxy() {
|
|
|
111
111
|
auth_part=""
|
|
112
112
|
fi
|
|
113
113
|
|
|
114
|
-
#
|
|
114
|
+
# Try in order: http → socks5 → https
|
|
115
115
|
local proto try_url
|
|
116
116
|
for proto in http socks5 https; do
|
|
117
117
|
try_url="${proto}://${auth_part}${host}:${port}"
|
|
@@ -121,7 +121,7 @@ _auto_detect_proxy() {
|
|
|
121
121
|
fi
|
|
122
122
|
done
|
|
123
123
|
|
|
124
|
-
#
|
|
124
|
+
# All failed, fallback to http
|
|
125
125
|
if [[ -n "$user" ]]; then
|
|
126
126
|
echo "http://${auth_part}${host}:${port}"
|
|
127
127
|
else
|
|
@@ -255,7 +255,7 @@ _require_setup() {
|
|
|
255
255
|
|
|
256
256
|
_require_env() {
|
|
257
257
|
[[ -d "$ENVS_DIR/$1" ]] || {
|
|
258
|
-
echo "
|
|
258
|
+
echo "error: environment '$1' not found, use 'cac ls' to list" >&2; exit 1
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -288,7 +288,7 @@ _install_method() {
|
|
|
288
288
|
local resolved="$self"
|
|
289
289
|
if [[ -L "$self" ]]; then
|
|
290
290
|
resolved=$(readlink "$self" 2>/dev/null || echo "$self")
|
|
291
|
-
#
|
|
291
|
+
# Handle relative symlinks
|
|
292
292
|
if [[ "$resolved" != /* ]]; then
|
|
293
293
|
resolved="$(dirname "$self")/$resolved"
|
|
294
294
|
fi
|
|
@@ -303,18 +303,18 @@ _install_method() {
|
|
|
303
303
|
_write_path_to_rc() {
|
|
304
304
|
local rc_file="${1:-$(_detect_rc_file)}"
|
|
305
305
|
if [[ -z "$rc_file" ]]; then
|
|
306
|
-
echo " $(_yellow '⚠')
|
|
306
|
+
echo " $(_yellow '⚠') shell config file not found, please add PATH manually:"
|
|
307
307
|
echo ' export PATH="$HOME/bin:$PATH"'
|
|
308
308
|
echo ' export PATH="$HOME/.cac/bin:$PATH"'
|
|
309
309
|
return 0
|
|
310
310
|
fi
|
|
311
311
|
|
|
312
312
|
if grep -q '# >>> cac >>>' "$rc_file" 2>/dev/null; then
|
|
313
|
-
echo " ✓ PATH
|
|
313
|
+
echo " ✓ PATH already exists in $rc_file, skipping"
|
|
314
314
|
return 0
|
|
315
315
|
fi
|
|
316
316
|
|
|
317
|
-
#
|
|
317
|
+
# Compat: remove old format if present
|
|
318
318
|
if grep -q '\.cac/bin' "$rc_file" 2>/dev/null; then
|
|
319
319
|
_remove_path_from_rc "$rc_file"
|
|
320
320
|
fi
|
|
@@ -336,7 +336,7 @@ cac() {
|
|
|
336
336
|
}
|
|
337
337
|
# <<< cac — Claude Code Cloak <<<
|
|
338
338
|
CACEOF
|
|
339
|
-
echo " ✓ PATH
|
|
339
|
+
echo " ✓ PATH written to $rc_file"
|
|
340
340
|
return 0
|
|
341
341
|
}
|
|
342
342
|
|
|
@@ -344,23 +344,23 @@ _remove_path_from_rc() {
|
|
|
344
344
|
local rc_file="${1:-$(_detect_rc_file)}"
|
|
345
345
|
[[ -z "$rc_file" ]] && return 0
|
|
346
346
|
|
|
347
|
-
#
|
|
347
|
+
# Remove marked block (new format)
|
|
348
348
|
if grep -q '# >>> cac' "$rc_file" 2>/dev/null; then
|
|
349
349
|
local tmp="${rc_file}.cac-tmp"
|
|
350
350
|
awk '/# >>> cac/{skip=1; next} /# <<< cac/{skip=0; next} !skip' "$rc_file" > "$tmp"
|
|
351
351
|
cat -s "$tmp" > "$rc_file"
|
|
352
352
|
rm -f "$tmp"
|
|
353
|
-
echo " ✓
|
|
353
|
+
echo " ✓ Removed PATH config from $rc_file"
|
|
354
354
|
return 0
|
|
355
355
|
fi
|
|
356
356
|
|
|
357
|
-
#
|
|
357
|
+
# Compat: old format
|
|
358
358
|
if grep -qE '(\.cac/bin|# cac —)' "$rc_file" 2>/dev/null; then
|
|
359
359
|
local tmp="${rc_file}.cac-tmp"
|
|
360
360
|
grep -vE '(# cac — Claude Code Cloak|\.cac/bin|# cac 命令|# claude wrapper)' "$rc_file" > "$tmp" || true
|
|
361
361
|
cat -s "$tmp" > "$rc_file"
|
|
362
362
|
rm -f "$tmp"
|
|
363
|
-
echo " ✓
|
|
363
|
+
echo " ✓ Removed PATH config from $rc_file (old format)"
|
|
364
364
|
return 0
|
|
365
365
|
fi
|
|
366
366
|
}
|
|
@@ -403,9 +403,9 @@ PYEOF
|
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
# ━━━ dns_block.sh ━━━
|
|
406
|
-
# ── DNS
|
|
406
|
+
# ── DNS interception & telemetry domain blocking ─────────────────────────────────────
|
|
407
407
|
|
|
408
|
-
#
|
|
408
|
+
# telemetry domains to block
|
|
409
409
|
TELEMETRY_DOMAINS=(
|
|
410
410
|
"statsig.anthropic.com"
|
|
411
411
|
"sentry.io"
|
|
@@ -413,8 +413,8 @@ TELEMETRY_DOMAINS=(
|
|
|
413
413
|
"cdn.growthbook.io"
|
|
414
414
|
)
|
|
415
415
|
|
|
416
|
-
#
|
|
417
|
-
# HOSTALIASES
|
|
416
|
+
# write HOSTALIASES file (fallback layer: gethostbyname-level blocking)
|
|
417
|
+
# HOSTALIASES format is hostname-to-hostname mapping (not IP)
|
|
418
418
|
_write_blocked_hosts() {
|
|
419
419
|
local hosts_file="$CAC_DIR/blocked_hosts"
|
|
420
420
|
{
|
|
@@ -426,14 +426,14 @@ _write_blocked_hosts() {
|
|
|
426
426
|
} > "$hosts_file"
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
#
|
|
429
|
+
# write Node.js DNS guard module (core layer: dns.lookup / dns.resolve level blocking + mTLS)
|
|
430
430
|
_write_dns_guard_js() {
|
|
431
431
|
local guard_file="$CAC_DIR/cac-dns-guard.js"
|
|
432
432
|
cat > "$guard_file" << 'DNSGUARD_EOF'
|
|
433
433
|
// ═══════════════════════════════════════════════════════════════
|
|
434
434
|
// cac-dns-guard.js
|
|
435
|
-
//
|
|
436
|
-
//
|
|
435
|
+
// DNS-level telemetry domain blocking | mTLS certificate injection | fetch leak prevention
|
|
436
|
+
// Injected into Claude Code process via NODE_OPTIONS="--require <this>"
|
|
437
437
|
// ═══════════════════════════════════════════════════════════════
|
|
438
438
|
'use strict';
|
|
439
439
|
|
|
@@ -444,7 +444,7 @@ var http = require('http');
|
|
|
444
444
|
var https = require('https');
|
|
445
445
|
var fs = require('fs');
|
|
446
446
|
|
|
447
|
-
// ─── 1.
|
|
447
|
+
// ─── 1. DNS-level telemetry domain blocking ──────────────────────────────────
|
|
448
448
|
|
|
449
449
|
var BLOCKED_DOMAINS = new Set([
|
|
450
450
|
'statsig.anthropic.com',
|
|
@@ -454,8 +454,8 @@ var BLOCKED_DOMAINS = new Set([
|
|
|
454
454
|
]);
|
|
455
455
|
|
|
456
456
|
/**
|
|
457
|
-
*
|
|
458
|
-
* e.g. "foo.sentry.io"
|
|
457
|
+
* Check if domain is in block list (including subdomain matching)
|
|
458
|
+
* e.g. "foo.sentry.io" matches "sentry.io"
|
|
459
459
|
*/
|
|
460
460
|
function isDomainBlocked(hostname) {
|
|
461
461
|
if (!hostname) return false;
|
|
@@ -478,7 +478,7 @@ function makeBlockedError(hostname, syscall) {
|
|
|
478
478
|
return err;
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
-
// ── 1a.
|
|
481
|
+
// ── 1a. intercept dns.lookup ──
|
|
482
482
|
var _origLookup = dns.lookup;
|
|
483
483
|
dns.lookup = function cacLookup(hostname, options, callback) {
|
|
484
484
|
if (typeof options === 'function') { callback = options; options = {}; }
|
|
@@ -490,7 +490,7 @@ dns.lookup = function cacLookup(hostname, options, callback) {
|
|
|
490
490
|
return _origLookup.call(dns, hostname, options, callback);
|
|
491
491
|
};
|
|
492
492
|
|
|
493
|
-
// ── 1b.
|
|
493
|
+
// ── 1b. intercept dns.resolve / resolve4 / resolve6 ──
|
|
494
494
|
['resolve','resolve4','resolve6'].forEach(function(method) {
|
|
495
495
|
var orig = dns[method];
|
|
496
496
|
if (!orig) return;
|
|
@@ -506,7 +506,7 @@ dns.lookup = function cacLookup(hostname, options, callback) {
|
|
|
506
506
|
};
|
|
507
507
|
});
|
|
508
508
|
|
|
509
|
-
// ── 1c.
|
|
509
|
+
// ── 1c. intercept dns.promises ──
|
|
510
510
|
if (dns.promises) {
|
|
511
511
|
var _origPLookup = dns.promises.lookup;
|
|
512
512
|
if (_origPLookup) {
|
|
@@ -525,7 +525,7 @@ if (dns.promises) {
|
|
|
525
525
|
});
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
// ── 1d.
|
|
528
|
+
// ── 1d. network layer safety net: intercept net.connect to telemetry domains ──
|
|
529
529
|
var _origNetConnect = net.connect;
|
|
530
530
|
var _origNetCreateConn = net.createConnection;
|
|
531
531
|
|
|
@@ -553,7 +553,7 @@ net.createConnection = function cacNetCreateConnection() {
|
|
|
553
553
|
};
|
|
554
554
|
|
|
555
555
|
|
|
556
|
-
// ─── 2. mTLS
|
|
556
|
+
// ─── 2. mTLS certificate injection ──────────────────────────────────
|
|
557
557
|
|
|
558
558
|
var mtlsCert = process.env.CAC_MTLS_CERT;
|
|
559
559
|
var mtlsKey = process.env.CAC_MTLS_KEY;
|
|
@@ -571,14 +571,14 @@ if (mtlsCert && mtlsKey) {
|
|
|
571
571
|
}
|
|
572
572
|
|
|
573
573
|
if (certData && keyData) {
|
|
574
|
-
//
|
|
574
|
+
// inject mTLS cert only for proxy connections
|
|
575
575
|
var proxyHost = proxyHostPort.split(':')[0];
|
|
576
576
|
var proxyPort = parseInt(proxyHostPort.split(':')[1], 10) || 0;
|
|
577
577
|
|
|
578
|
-
// 2a.
|
|
578
|
+
// 2a. intercept tls.connect, inject client cert for proxy connections
|
|
579
579
|
var _origTlsConnect = tls.connect;
|
|
580
580
|
tls.connect = function cacTlsConnect() {
|
|
581
|
-
//
|
|
581
|
+
// normalize parameters: tls.connect(options[, cb]) or tls.connect(port[, host][, options][, cb])
|
|
582
582
|
var args = Array.prototype.slice.call(arguments);
|
|
583
583
|
var options, callback;
|
|
584
584
|
|
|
@@ -586,7 +586,7 @@ if (mtlsCert && mtlsKey) {
|
|
|
586
586
|
options = args[0];
|
|
587
587
|
callback = (typeof args[1] === 'function') ? args[1] : undefined;
|
|
588
588
|
} else {
|
|
589
|
-
// tls.connect(port, host, options, cb)
|
|
589
|
+
// tls.connect(port, host, options, cb) form
|
|
590
590
|
var port = args[0];
|
|
591
591
|
var host = (typeof args[1] === 'string') ? args[1] : 'localhost';
|
|
592
592
|
var optIdx = (typeof args[1] === 'string') ? 2 : 1;
|
|
@@ -597,7 +597,7 @@ if (mtlsCert && mtlsKey) {
|
|
|
597
597
|
if (typeof callback !== 'function') callback = undefined;
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
//
|
|
600
|
+
// inject only for proxy connections (exact host:port match)
|
|
601
601
|
var targetHost = options.host || options.hostname || '';
|
|
602
602
|
var targetPort = options.port || 0;
|
|
603
603
|
if (proxyHost && targetHost === proxyHost &&
|
|
@@ -617,7 +617,7 @@ if (mtlsCert && mtlsKey) {
|
|
|
617
617
|
return _origTlsConnect.call(tls, options);
|
|
618
618
|
};
|
|
619
619
|
|
|
620
|
-
// 2b.
|
|
620
|
+
// 2b. inject CA into https.globalAgent (trust CA only, no client private key)
|
|
621
621
|
if (caData && https.globalAgent && https.globalAgent.options) {
|
|
622
622
|
https.globalAgent.options.ca = https.globalAgent.options.ca
|
|
623
623
|
? [].concat(https.globalAgent.options.ca, caData)
|
|
@@ -627,15 +627,15 @@ if (mtlsCert && mtlsKey) {
|
|
|
627
627
|
}
|
|
628
628
|
|
|
629
629
|
|
|
630
|
-
// ─── 3. fetch
|
|
631
|
-
// Node.js
|
|
632
|
-
//
|
|
633
|
-
//
|
|
630
|
+
// ─── 3. fetch telemetry interception patch ──────────────────────────────────
|
|
631
|
+
// Node.js native fetch (undici) bypasses dns.lookup, circumventing DNS blocking
|
|
632
|
+
// Strategy: prefer node-fetch (uses http/https modules -> dns.lookup)
|
|
633
|
+
// otherwise wrap native fetch to block telemetry domains
|
|
634
634
|
|
|
635
635
|
(function patchFetch() {
|
|
636
636
|
if (typeof globalThis === 'undefined') return;
|
|
637
637
|
|
|
638
|
-
//
|
|
638
|
+
// prefer node-fetch (based on http/https modules, naturally goes through dns.lookup interception chain)
|
|
639
639
|
try {
|
|
640
640
|
var nodeFetch = require('node-fetch');
|
|
641
641
|
if (nodeFetch && typeof nodeFetch === 'function') {
|
|
@@ -646,10 +646,10 @@ if (mtlsCert && mtlsKey) {
|
|
|
646
646
|
return;
|
|
647
647
|
}
|
|
648
648
|
} catch(e) {
|
|
649
|
-
// node-fetch
|
|
649
|
+
// node-fetch not available
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
-
//
|
|
652
|
+
// fallback: wrap native fetch to ensure telemetry domains are blocked
|
|
653
653
|
if (typeof globalThis.fetch === 'function') {
|
|
654
654
|
var _origFetch = globalThis.fetch;
|
|
655
655
|
globalThis.fetch = function cacFetch(input, init) {
|
|
@@ -669,15 +669,15 @@ if (mtlsCert && mtlsKey) {
|
|
|
669
669
|
})();
|
|
670
670
|
|
|
671
671
|
|
|
672
|
-
// ─── 4.
|
|
673
|
-
// Claude Code
|
|
674
|
-
// Cloudflare
|
|
675
|
-
//
|
|
676
|
-
//
|
|
672
|
+
// ─── 4. health check bypass (in-process interception, URL-specific) ────────────────
|
|
673
|
+
// Claude Code pings https://api.anthropic.com/api/hello on startup
|
|
674
|
+
// Cloudflare blocks Node.js TLS fingerprint (JA3/JA4) -> 403
|
|
675
|
+
// Solution: intercept this request at Node.js layer, return 200 directly, no network traffic
|
|
676
|
+
// Only intercepts health check, does not affect OAuth/API or other requests
|
|
677
677
|
|
|
678
678
|
function isHealthCheck(url) {
|
|
679
679
|
if (!url) return false;
|
|
680
|
-
//
|
|
680
|
+
// match https://api.anthropic.com/api/hello or variants with query params
|
|
681
681
|
return /^https?:\/\/api\.anthropic\.com\/api\/hello/.test(url);
|
|
682
682
|
}
|
|
683
683
|
|
|
@@ -717,7 +717,7 @@ function makeHealthResponse(callback) {
|
|
|
717
717
|
return req;
|
|
718
718
|
}
|
|
719
719
|
|
|
720
|
-
// 4a.
|
|
720
|
+
// 4a. intercept https.get / https.request
|
|
721
721
|
var _origHttpsRequest = https.request;
|
|
722
722
|
var _origHttpsGet = https.get;
|
|
723
723
|
|
|
@@ -755,7 +755,7 @@ https.get = function cacHttpsGet(urlOrOpts, optsOrCb, cb) {
|
|
|
755
755
|
return _origHttpsGet.apply(https, arguments);
|
|
756
756
|
};
|
|
757
757
|
|
|
758
|
-
// 4b.
|
|
758
|
+
// 4b. intercept fetch (undici bypasses https module)
|
|
759
759
|
(function patchHealthFetch() {
|
|
760
760
|
if (typeof globalThis === 'undefined' || typeof globalThis.fetch !== 'function') return;
|
|
761
761
|
var _prevFetch = globalThis.fetch;
|
|
@@ -777,13 +777,13 @@ DNSGUARD_EOF
|
|
|
777
777
|
chmod 644 "$guard_file"
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
-
#
|
|
780
|
+
# verify DNS blocking is active
|
|
781
781
|
_check_dns_block() {
|
|
782
782
|
local domain="${1:-statsig.anthropic.com}"
|
|
783
783
|
local guard_file="$CAC_DIR/cac-dns-guard.js"
|
|
784
784
|
|
|
785
785
|
if [[ ! -f "$guard_file" ]]; then
|
|
786
|
-
echo "$(_red "✗") DNS guard
|
|
786
|
+
echo "$(_red "✗") DNS guard module not found"
|
|
787
787
|
return 1
|
|
788
788
|
fi
|
|
789
789
|
|
|
@@ -797,37 +797,37 @@ _check_dns_block() {
|
|
|
797
797
|
' "$guard_file" "$domain" 2>/dev/null || echo "ERROR")
|
|
798
798
|
|
|
799
799
|
if [[ "$result" == "BLOCKED" ]]; then
|
|
800
|
-
echo "$(_green "✓") $domain
|
|
800
|
+
echo "$(_green "✓") $domain blocked"
|
|
801
801
|
return 0
|
|
802
802
|
else
|
|
803
|
-
echo "$(_red "✗") $domain
|
|
803
|
+
echo "$(_red "✗") $domain not blocked ($result)"
|
|
804
804
|
return 1
|
|
805
805
|
fi
|
|
806
806
|
}
|
|
807
807
|
|
|
808
808
|
# ━━━ mtls.sh ━━━
|
|
809
|
-
# ── mTLS
|
|
809
|
+
# ── mTLS client certificate management ─────────────────────────────────────────
|
|
810
810
|
|
|
811
|
-
#
|
|
811
|
+
# generate self-signed CA (called during setup, generated only once)
|
|
812
812
|
_generate_ca_cert() {
|
|
813
813
|
local ca_dir="$CAC_DIR/ca"
|
|
814
814
|
local ca_key="$ca_dir/ca_key.pem"
|
|
815
815
|
local ca_cert="$ca_dir/ca_cert.pem"
|
|
816
816
|
|
|
817
817
|
if [[ -f "$ca_cert" ]] && [[ -f "$ca_key" ]]; then
|
|
818
|
-
echo " CA
|
|
818
|
+
echo " CA cert exists, skipping"
|
|
819
819
|
return 0
|
|
820
820
|
fi
|
|
821
821
|
|
|
822
822
|
mkdir -p "$ca_dir"
|
|
823
823
|
|
|
824
|
-
#
|
|
824
|
+
# generate CA private key (4096-bit RSA)
|
|
825
825
|
openssl genrsa -out "$ca_key" 4096 2>/dev/null || {
|
|
826
|
-
echo "
|
|
826
|
+
echo "error: failed to generate CA private key" >&2; return 1
|
|
827
827
|
}
|
|
828
828
|
chmod 600 "$ca_key"
|
|
829
829
|
|
|
830
|
-
#
|
|
830
|
+
# generate self-signed CA cert (valid for 10 years)
|
|
831
831
|
openssl req -new -x509 \
|
|
832
832
|
-key "$ca_key" \
|
|
833
833
|
-out "$ca_cert" \
|
|
@@ -836,12 +836,12 @@ _generate_ca_cert() {
|
|
|
836
836
|
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
|
|
837
837
|
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
|
838
838
|
2>/dev/null || {
|
|
839
|
-
echo "
|
|
839
|
+
echo "error: failed to generate CA cert" >&2; return 1
|
|
840
840
|
}
|
|
841
841
|
chmod 644 "$ca_cert"
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
#
|
|
844
|
+
# generate client cert for environment (called during cac add)
|
|
845
845
|
_generate_client_cert() {
|
|
846
846
|
local name="$1"
|
|
847
847
|
local env_dir="$ENVS_DIR/$name"
|
|
@@ -849,7 +849,7 @@ _generate_client_cert() {
|
|
|
849
849
|
local ca_cert="$CAC_DIR/ca/ca_cert.pem"
|
|
850
850
|
|
|
851
851
|
if [[ ! -f "$ca_key" ]] || [[ ! -f "$ca_cert" ]]; then
|
|
852
|
-
echo "
|
|
852
|
+
echo " warning: CA cert not found, skipping client cert generation" >&2
|
|
853
853
|
return 1
|
|
854
854
|
fi
|
|
855
855
|
|
|
@@ -857,22 +857,22 @@ _generate_client_cert() {
|
|
|
857
857
|
local client_csr="$env_dir/client_csr.pem"
|
|
858
858
|
local client_cert="$env_dir/client_cert.pem"
|
|
859
859
|
|
|
860
|
-
#
|
|
860
|
+
# generate client private key (2048-bit RSA)
|
|
861
861
|
openssl genrsa -out "$client_key" 2048 2>/dev/null || {
|
|
862
|
-
echo "
|
|
862
|
+
echo "error: failed to generate client private key" >&2; return 1
|
|
863
863
|
}
|
|
864
864
|
chmod 600 "$client_key"
|
|
865
865
|
|
|
866
|
-
#
|
|
866
|
+
# generate CSR
|
|
867
867
|
openssl req -new \
|
|
868
868
|
-key "$client_key" \
|
|
869
869
|
-out "$client_csr" \
|
|
870
870
|
-subj "/CN=cac-client-${name}/O=cac/OU=env-${name}" \
|
|
871
871
|
2>/dev/null || {
|
|
872
|
-
echo "
|
|
872
|
+
echo "error: failed to generate CSR" >&2; return 1
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
-
#
|
|
875
|
+
# sign client cert with CA (valid for 1 year)
|
|
876
876
|
openssl x509 -req \
|
|
877
877
|
-in "$client_csr" \
|
|
878
878
|
-CA "$ca_cert" \
|
|
@@ -882,52 +882,52 @@ _generate_client_cert() {
|
|
|
882
882
|
-days 365 \
|
|
883
883
|
-extfile <(printf "keyUsage=critical,digitalSignature\nextendedKeyUsage=clientAuth") \
|
|
884
884
|
2>/dev/null || {
|
|
885
|
-
echo "
|
|
885
|
+
echo "error: failed to sign client cert" >&2; return 1
|
|
886
886
|
}
|
|
887
887
|
chmod 644 "$client_cert"
|
|
888
888
|
|
|
889
|
-
#
|
|
889
|
+
# cleanup CSR (no longer needed)
|
|
890
890
|
rm -f "$client_csr"
|
|
891
891
|
}
|
|
892
892
|
|
|
893
|
-
#
|
|
893
|
+
# verify mTLS certificate status
|
|
894
894
|
_check_mtls() {
|
|
895
895
|
local env_dir="$1"
|
|
896
896
|
local ca_cert="$CAC_DIR/ca/ca_cert.pem"
|
|
897
897
|
local client_cert="$env_dir/client_cert.pem"
|
|
898
898
|
local client_key="$env_dir/client_key.pem"
|
|
899
899
|
|
|
900
|
-
#
|
|
900
|
+
# check CA
|
|
901
901
|
if [[ ! -f "$ca_cert" ]]; then
|
|
902
|
-
echo "$(_red "✗") CA
|
|
902
|
+
echo "$(_red "✗") CA cert not found"
|
|
903
903
|
return 1
|
|
904
904
|
fi
|
|
905
905
|
|
|
906
|
-
#
|
|
906
|
+
# check client cert
|
|
907
907
|
if [[ ! -f "$client_cert" ]] || [[ ! -f "$client_key" ]]; then
|
|
908
|
-
echo "$(_yellow "⚠")
|
|
908
|
+
echo "$(_yellow "⚠") client cert not found"
|
|
909
909
|
return 1
|
|
910
910
|
fi
|
|
911
911
|
|
|
912
|
-
#
|
|
912
|
+
# verify certificate chain
|
|
913
913
|
if openssl verify -CAfile "$ca_cert" "$client_cert" >/dev/null 2>&1; then
|
|
914
|
-
#
|
|
914
|
+
# check certificate expiry
|
|
915
915
|
local expiry
|
|
916
916
|
expiry=$(openssl x509 -in "$client_cert" -noout -enddate 2>/dev/null | cut -d= -f2)
|
|
917
917
|
local cn
|
|
918
918
|
cn=$(openssl x509 -in "$client_cert" -noout -subject 2>/dev/null | sed 's/.*CN *= *//')
|
|
919
|
-
echo "$(_green "✓") mTLS
|
|
919
|
+
echo "$(_green "✓") mTLS certificate valid (CN=$cn, expires: $expiry)"
|
|
920
920
|
return 0
|
|
921
921
|
else
|
|
922
|
-
echo "$(_red "✗")
|
|
922
|
+
echo "$(_red "✗") certificate chain verification failed"
|
|
923
923
|
return 1
|
|
924
924
|
fi
|
|
925
925
|
}
|
|
926
926
|
|
|
927
927
|
# ━━━ templates.sh ━━━
|
|
928
|
-
# ── templates:
|
|
928
|
+
# ── templates: wrapper, shim, env init ──────────────────
|
|
929
929
|
|
|
930
|
-
#
|
|
930
|
+
# write statusline-command.sh to env .claude dir
|
|
931
931
|
_write_statusline_script() {
|
|
932
932
|
local config_dir="$1"
|
|
933
933
|
cat > "$config_dir/statusline-command.sh" << 'STATUSLINE_EOF'
|
|
@@ -1020,7 +1020,7 @@ STATUSLINE_EOF
|
|
|
1020
1020
|
chmod +x "$config_dir/statusline-command.sh"
|
|
1021
1021
|
}
|
|
1022
1022
|
|
|
1023
|
-
#
|
|
1023
|
+
# write settings.json to env .claude dir
|
|
1024
1024
|
_write_env_settings() {
|
|
1025
1025
|
local config_dir="$1"
|
|
1026
1026
|
cat > "$config_dir/settings.json" << 'SETTINGS_EOF'
|
|
@@ -1037,7 +1037,7 @@ _write_env_settings() {
|
|
|
1037
1037
|
SETTINGS_EOF
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
|
-
#
|
|
1040
|
+
# write CLAUDE.md to env .claude dir
|
|
1041
1041
|
_write_env_claude_md() {
|
|
1042
1042
|
local config_dir="$1"
|
|
1043
1043
|
local env_name="$2"
|
|
@@ -1066,25 +1066,25 @@ set -euo pipefail
|
|
|
1066
1066
|
CAC_DIR="$HOME/.cac"
|
|
1067
1067
|
ENVS_DIR="$CAC_DIR/envs"
|
|
1068
1068
|
|
|
1069
|
-
# cacstop
|
|
1069
|
+
# cacstop state: passthrough directly
|
|
1070
1070
|
if [[ -f "$CAC_DIR/stopped" ]]; then
|
|
1071
1071
|
_real=$(tr -d '[:space:]' < "$CAC_DIR/real_claude" 2>/dev/null || true)
|
|
1072
1072
|
[[ -x "$_real" ]] && exec "$_real" "$@"
|
|
1073
|
-
echo "[cac]
|
|
1073
|
+
echo "[cac] error: real claude not found, run 'cac setup'" >&2; exit 1
|
|
1074
1074
|
fi
|
|
1075
1075
|
|
|
1076
|
-
#
|
|
1076
|
+
# read current environment
|
|
1077
1077
|
if [[ ! -f "$CAC_DIR/current" ]]; then
|
|
1078
|
-
echo "[cac]
|
|
1078
|
+
echo "[cac] error: no active environment, run 'cac <name>'" >&2; exit 1
|
|
1079
1079
|
fi
|
|
1080
1080
|
_name=$(tr -d '[:space:]' < "$CAC_DIR/current")
|
|
1081
1081
|
_env_dir="$ENVS_DIR/$_name"
|
|
1082
|
-
[[ -d "$_env_dir" ]] || { echo "[cac]
|
|
1082
|
+
[[ -d "$_env_dir" ]] || { echo "[cac] error: environment '$_name' not found" >&2; exit 1; }
|
|
1083
1083
|
|
|
1084
1084
|
# Isolated .claude config directory
|
|
1085
1085
|
if [[ -d "$_env_dir/.claude" ]]; then
|
|
1086
1086
|
export CLAUDE_CONFIG_DIR="$_env_dir/.claude"
|
|
1087
|
-
#
|
|
1087
|
+
# ensure settings.json exists, prevent Claude Code fallback to ~/.claude/settings.json
|
|
1088
1088
|
[[ -f "$_env_dir/.claude/settings.json" ]] || echo '{}' > "$_env_dir/.claude/settings.json"
|
|
1089
1089
|
fi
|
|
1090
1090
|
|
|
@@ -1095,18 +1095,18 @@ if [[ -f "$_env_dir/proxy" ]]; then
|
|
|
1095
1095
|
fi
|
|
1096
1096
|
|
|
1097
1097
|
if [[ -n "$PROXY" ]]; then
|
|
1098
|
-
# pre-flight
|
|
1098
|
+
# pre-flight: proxy connectivity (pure bash, no fork)
|
|
1099
1099
|
_hp="${PROXY##*@}"; _hp="${_hp##*://}"
|
|
1100
1100
|
_host="${_hp%%:*}"
|
|
1101
1101
|
_port="${_hp##*:}"
|
|
1102
1102
|
if ! (echo >/dev/tcp/"$_host"/"$_port") 2>/dev/null; then
|
|
1103
|
-
echo "[cac]
|
|
1104
|
-
echo "[cac]
|
|
1103
|
+
echo "[cac] error: [$_name] proxy $_hp unreachable, refusing to start." >&2
|
|
1104
|
+
echo "[cac] hint: run 'cac check' to diagnose, or 'cac stop' to disable temporarily" >&2
|
|
1105
1105
|
exit 1
|
|
1106
1106
|
fi
|
|
1107
1107
|
fi
|
|
1108
1108
|
|
|
1109
|
-
#
|
|
1109
|
+
# inject statsig stable_id
|
|
1110
1110
|
if [[ -f "$_env_dir/stable_id" ]]; then
|
|
1111
1111
|
_sid=$(tr -d '[:space:]' < "$_env_dir/stable_id")
|
|
1112
1112
|
_config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
@@ -1119,7 +1119,7 @@ if [[ -f "$_env_dir/stable_id" ]]; then
|
|
|
1119
1119
|
fi
|
|
1120
1120
|
fi
|
|
1121
1121
|
|
|
1122
|
-
#
|
|
1122
|
+
# inject env vars — proxy (only when proxy is configured)
|
|
1123
1123
|
if [[ -n "$PROXY" ]]; then
|
|
1124
1124
|
export _CAC_PROXY="$PROXY"
|
|
1125
1125
|
export HTTPS_PROXY="$PROXY" HTTP_PROXY="$PROXY" ALL_PROXY="$PROXY"
|
|
@@ -1127,45 +1127,49 @@ if [[ -n "$PROXY" ]]; then
|
|
|
1127
1127
|
fi
|
|
1128
1128
|
export PATH="$CAC_DIR/shim-bin:$PATH"
|
|
1129
1129
|
|
|
1130
|
-
# ──
|
|
1131
|
-
# Layer 1: Claude Code
|
|
1130
|
+
# ── multi-layer telemetry protection ──
|
|
1131
|
+
# Layer 1: Claude Code native toggle
|
|
1132
1132
|
export CLAUDE_CODE_ENABLE_TELEMETRY=
|
|
1133
|
-
# Layer 2:
|
|
1133
|
+
# Layer 2: universal telemetry opt-out (https://consoledonottrack.com)
|
|
1134
1134
|
export DO_NOT_TRACK=1
|
|
1135
|
-
# Layer 3: OpenTelemetry SDK
|
|
1135
|
+
# Layer 3: OpenTelemetry SDK fully disabled
|
|
1136
1136
|
export OTEL_SDK_DISABLED=true
|
|
1137
1137
|
export OTEL_TRACES_EXPORTER=none
|
|
1138
1138
|
export OTEL_METRICS_EXPORTER=none
|
|
1139
1139
|
export OTEL_LOGS_EXPORTER=none
|
|
1140
|
-
# Layer 4: Sentry DSN
|
|
1140
|
+
# Layer 4: empty Sentry DSN, block error reporting
|
|
1141
1141
|
export SENTRY_DSN=
|
|
1142
|
-
# Layer 5: Claude Code
|
|
1142
|
+
# Layer 5: Claude Code specific toggles
|
|
1143
1143
|
export DISABLE_ERROR_REPORTING=1
|
|
1144
1144
|
export DISABLE_BUG_COMMAND=1
|
|
1145
1145
|
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
|
1146
|
-
# Layer 6:
|
|
1146
|
+
# Layer 6: other known telemetry flags
|
|
1147
1147
|
export TELEMETRY_DISABLED=1
|
|
1148
1148
|
export DISABLE_TELEMETRY=1
|
|
1149
1149
|
|
|
1150
|
-
#
|
|
1151
|
-
#
|
|
1150
|
+
# with proxy: force OAuth (clear API config to prevent leaks)
|
|
1151
|
+
# without proxy: preserve user's API Key / Base URL
|
|
1152
1152
|
if [[ -n "$PROXY" ]]; then
|
|
1153
1153
|
unset ANTHROPIC_BASE_URL
|
|
1154
1154
|
unset ANTHROPIC_AUTH_TOKEN
|
|
1155
1155
|
unset ANTHROPIC_API_KEY
|
|
1156
1156
|
fi
|
|
1157
1157
|
|
|
1158
|
-
# ── NS
|
|
1158
|
+
# ── NS-level DNS interception ──
|
|
1159
1159
|
if [[ -f "$CAC_DIR/cac-dns-guard.js" ]]; then
|
|
1160
1160
|
case "${NODE_OPTIONS:-}" in
|
|
1161
|
-
*cac-dns-guard.js*) ;; #
|
|
1161
|
+
*cac-dns-guard.js*) ;; # already injected, skip
|
|
1162
1162
|
*) export NODE_OPTIONS="${NODE_OPTIONS:-} --require $CAC_DIR/cac-dns-guard.js" ;;
|
|
1163
1163
|
esac
|
|
1164
|
+
case "${BUN_OPTIONS:-}" in
|
|
1165
|
+
*cac-dns-guard.js*) ;;
|
|
1166
|
+
*) export BUN_OPTIONS="${BUN_OPTIONS:-} --preload $CAC_DIR/cac-dns-guard.js" ;;
|
|
1167
|
+
esac
|
|
1164
1168
|
fi
|
|
1165
|
-
#
|
|
1169
|
+
# fallback layer: HOSTALIASES (gethostbyname level)
|
|
1166
1170
|
[[ -f "$CAC_DIR/blocked_hosts" ]] && export HOSTALIASES="$CAC_DIR/blocked_hosts"
|
|
1167
1171
|
|
|
1168
|
-
# ── mTLS
|
|
1172
|
+
# ── mTLS client certificate ──
|
|
1169
1173
|
if [[ -f "$_env_dir/client_cert.pem" ]] && [[ -f "$_env_dir/client_key.pem" ]]; then
|
|
1170
1174
|
export CAC_MTLS_CERT="$_env_dir/client_cert.pem"
|
|
1171
1175
|
export CAC_MTLS_KEY="$_env_dir/client_key.pem"
|
|
@@ -1176,7 +1180,7 @@ if [[ -f "$_env_dir/client_cert.pem" ]] && [[ -f "$_env_dir/client_key.pem" ]];
|
|
|
1176
1180
|
[[ -n "${_hp:-}" ]] && export CAC_PROXY_HOST="$_hp"
|
|
1177
1181
|
fi
|
|
1178
1182
|
|
|
1179
|
-
#
|
|
1183
|
+
# ensure CA cert is always trusted (required for mTLS)
|
|
1180
1184
|
[[ -f "$CAC_DIR/ca/ca_cert.pem" ]] && export NODE_EXTRA_CA_CERTS="$CAC_DIR/ca/ca_cert.pem"
|
|
1181
1185
|
|
|
1182
1186
|
[[ -f "$_env_dir/tz" ]] && export TZ=$(tr -d '[:space:]' < "$_env_dir/tz")
|
|
@@ -1186,18 +1190,22 @@ if [[ -f "$_env_dir/hostname" ]]; then
|
|
|
1186
1190
|
export HOSTNAME="$_hn" CAC_HOSTNAME="$_hn"
|
|
1187
1191
|
fi
|
|
1188
1192
|
|
|
1189
|
-
# Node.js
|
|
1193
|
+
# Node.js-level fingerprint interception (bypasses shell shim limitations)
|
|
1190
1194
|
[[ -f "$_env_dir/mac_address" ]] && export CAC_MAC=$(tr -d '[:space:]' < "$_env_dir/mac_address")
|
|
1191
1195
|
[[ -f "$_env_dir/machine_id" ]] && export CAC_MACHINE_ID=$(tr -d '[:space:]' < "$_env_dir/machine_id")
|
|
1192
1196
|
export CAC_USERNAME="user-$(echo "$_name" | cut -c1-8)"
|
|
1193
1197
|
if [[ -f "$CAC_DIR/fingerprint-hook.js" ]]; then
|
|
1194
1198
|
case "${NODE_OPTIONS:-}" in
|
|
1195
|
-
*fingerprint-hook.js*) ;;
|
|
1199
|
+
*fingerprint-hook.js*) ;;
|
|
1196
1200
|
*) export NODE_OPTIONS="--require $CAC_DIR/fingerprint-hook.js ${NODE_OPTIONS:-}" ;;
|
|
1197
1201
|
esac
|
|
1202
|
+
case "${BUN_OPTIONS:-}" in
|
|
1203
|
+
*fingerprint-hook.js*) ;;
|
|
1204
|
+
*) export BUN_OPTIONS="--preload $CAC_DIR/fingerprint-hook.js ${BUN_OPTIONS:-}" ;;
|
|
1205
|
+
esac
|
|
1198
1206
|
fi
|
|
1199
1207
|
|
|
1200
|
-
#
|
|
1208
|
+
# exec real claude — versioned binary or system fallback
|
|
1201
1209
|
_real=""
|
|
1202
1210
|
if [[ -f "$_env_dir/version" ]]; then
|
|
1203
1211
|
_ver=$(tr -d '[:space:]' < "$_env_dir/version")
|
|
@@ -1207,23 +1215,23 @@ fi
|
|
|
1207
1215
|
if [[ -z "$_real" ]] || [[ ! -x "$_real" ]]; then
|
|
1208
1216
|
_real=$(tr -d '[:space:]' < "$CAC_DIR/real_claude")
|
|
1209
1217
|
fi
|
|
1210
|
-
[[ -x "$_real" ]] || { echo "[cac]
|
|
1218
|
+
[[ -x "$_real" ]] || { echo "[cac] error: claude not found, run 'cac setup'" >&2; exit 1; }
|
|
1211
1219
|
|
|
1212
|
-
# ── Relay
|
|
1220
|
+
# ── Relay local forwarding (always enabled when proxy is set) ──
|
|
1213
1221
|
_relay_active=false
|
|
1214
1222
|
if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
1215
1223
|
_relay_js="$CAC_DIR/relay.js"
|
|
1216
1224
|
_relay_pid_file="$CAC_DIR/relay.pid"
|
|
1217
1225
|
_relay_port_file="$CAC_DIR/relay.port"
|
|
1218
1226
|
|
|
1219
|
-
#
|
|
1227
|
+
# check if relay is already running
|
|
1220
1228
|
_relay_running=false
|
|
1221
1229
|
if [[ -f "$_relay_pid_file" ]]; then
|
|
1222
1230
|
_rpid=$(tr -d '[:space:]' < "$_relay_pid_file")
|
|
1223
1231
|
kill -0 "$_rpid" 2>/dev/null && _relay_running=true
|
|
1224
1232
|
fi
|
|
1225
1233
|
|
|
1226
|
-
#
|
|
1234
|
+
# start if not running
|
|
1227
1235
|
if [[ "$_relay_running" != "true" ]] && [[ -f "$_relay_js" ]]; then
|
|
1228
1236
|
_rport=17890
|
|
1229
1237
|
while (echo >/dev/tcp/127.0.0.1/$_rport) 2>/dev/null; do
|
|
@@ -1239,7 +1247,7 @@ if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
|
1239
1247
|
echo "$_rport" > "$_relay_port_file"
|
|
1240
1248
|
fi
|
|
1241
1249
|
|
|
1242
|
-
#
|
|
1250
|
+
# override proxy to point to local relay
|
|
1243
1251
|
if [[ -f "$_relay_port_file" ]]; then
|
|
1244
1252
|
_rport=$(tr -d '[:space:]' < "$_relay_port_file")
|
|
1245
1253
|
export HTTPS_PROXY="http://127.0.0.1:$_rport"
|
|
@@ -1249,9 +1257,9 @@ if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
|
1249
1257
|
fi
|
|
1250
1258
|
fi
|
|
1251
1259
|
|
|
1252
|
-
#
|
|
1260
|
+
# cleanup function
|
|
1253
1261
|
_cleanup_all() {
|
|
1254
|
-
#
|
|
1262
|
+
# cleanup relay
|
|
1255
1263
|
if [[ "$_relay_active" == "true" ]] && [[ -f "$CAC_DIR/relay.pid" ]]; then
|
|
1256
1264
|
local _p; _p=$(cat "$CAC_DIR/relay.pid" 2>/dev/null) || true
|
|
1257
1265
|
[[ -n "$_p" ]] && kill "$_p" 2>/dev/null || true
|
|
@@ -1274,7 +1282,7 @@ _write_ioreg_shim() {
|
|
|
1274
1282
|
#!/usr/bin/env bash
|
|
1275
1283
|
CAC_DIR="$HOME/.cac"
|
|
1276
1284
|
|
|
1277
|
-
#
|
|
1285
|
+
# non-target call: passthrough to real ioreg
|
|
1278
1286
|
if ! echo "$*" | grep -q "IOPlatformExpertDevice"; then
|
|
1279
1287
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
|
|
1280
1288
|
command -v ioreg 2>/dev/null || true)
|
|
@@ -1282,7 +1290,7 @@ if ! echo "$*" | grep -q "IOPlatformExpertDevice"; then
|
|
|
1282
1290
|
exit 0
|
|
1283
1291
|
fi
|
|
1284
1292
|
|
|
1285
|
-
#
|
|
1293
|
+
# read current env UUID
|
|
1286
1294
|
_uuid_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/uuid"
|
|
1287
1295
|
if [[ ! -f "$_uuid_file" ]]; then
|
|
1288
1296
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
|
|
@@ -1312,10 +1320,10 @@ _write_machine_id_shim() {
|
|
|
1312
1320
|
#!/usr/bin/env bash
|
|
1313
1321
|
CAC_DIR="$HOME/.cac"
|
|
1314
1322
|
|
|
1315
|
-
#
|
|
1323
|
+
# get real cat path first (avoid recursive self-call)
|
|
1316
1324
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v cat 2>/dev/null || true)
|
|
1317
1325
|
|
|
1318
|
-
#
|
|
1326
|
+
# intercept /etc/machine-id and /var/lib/dbus/machine-id
|
|
1319
1327
|
if [[ "$1" == "/etc/machine-id" ]] || [[ "$1" == "/var/lib/dbus/machine-id" ]]; then
|
|
1320
1328
|
_mid_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/machine_id"
|
|
1321
1329
|
if [[ -f "$_mid_file" ]] && [[ -n "$_real" ]]; then
|
|
@@ -1323,7 +1331,7 @@ if [[ "$1" == "/etc/machine-id" ]] || [[ "$1" == "/var/lib/dbus/machine-id" ]];
|
|
|
1323
1331
|
fi
|
|
1324
1332
|
fi
|
|
1325
1333
|
|
|
1326
|
-
#
|
|
1334
|
+
# non-target call: passthrough to real cat
|
|
1327
1335
|
[[ -n "$_real" ]] && exec "$_real" "$@"
|
|
1328
1336
|
exit 1
|
|
1329
1337
|
CAT_EOF
|
|
@@ -1336,14 +1344,14 @@ _write_hostname_shim() {
|
|
|
1336
1344
|
#!/usr/bin/env bash
|
|
1337
1345
|
CAC_DIR="$HOME/.cac"
|
|
1338
1346
|
|
|
1339
|
-
#
|
|
1347
|
+
# read spoofed hostname
|
|
1340
1348
|
_hn_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/hostname"
|
|
1341
1349
|
if [[ -f "$_hn_file" ]]; then
|
|
1342
1350
|
tr -d '[:space:]' < "$_hn_file"
|
|
1343
1351
|
exit 0
|
|
1344
1352
|
fi
|
|
1345
1353
|
|
|
1346
|
-
#
|
|
1354
|
+
# passthrough to real hostname
|
|
1347
1355
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v hostname 2>/dev/null || true)
|
|
1348
1356
|
[[ -n "$_real" ]] && exec "$_real" "$@"
|
|
1349
1357
|
exit 1
|
|
@@ -1788,7 +1796,7 @@ cmd_env() {
|
|
|
1788
1796
|
}
|
|
1789
1797
|
|
|
1790
1798
|
# ━━━ cmd_relay.sh ━━━
|
|
1791
|
-
# ── cmd: relay
|
|
1799
|
+
# ── cmd: relay (local relay, bypass TUN) ──────────────────────────────
|
|
1792
1800
|
|
|
1793
1801
|
_relay_start() {
|
|
1794
1802
|
local name="${1:-$(_current_env)}"
|
|
@@ -1797,14 +1805,14 @@ _relay_start() {
|
|
|
1797
1805
|
[[ -z "$proxy" ]] && return 1
|
|
1798
1806
|
|
|
1799
1807
|
local relay_js="$CAC_DIR/relay.js"
|
|
1800
|
-
[[ -f "$relay_js" ]] || { echo "
|
|
1808
|
+
[[ -f "$relay_js" ]] || { echo "error: relay.js not found, run 'cac setup'" >&2; return 1; }
|
|
1801
1809
|
|
|
1802
|
-
#
|
|
1810
|
+
# find available port (17890-17999)
|
|
1803
1811
|
local port=17890
|
|
1804
1812
|
while (echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null; do
|
|
1805
1813
|
(( port++ ))
|
|
1806
1814
|
if [[ $port -gt 17999 ]]; then
|
|
1807
|
-
echo "
|
|
1815
|
+
echo "error: all ports 17890-17999 occupied" >&2
|
|
1808
1816
|
return 1
|
|
1809
1817
|
fi
|
|
1810
1818
|
done
|
|
@@ -1813,7 +1821,7 @@ _relay_start() {
|
|
|
1813
1821
|
node "$relay_js" "$port" "$proxy" "$pid_file" </dev/null >"$CAC_DIR/relay.log" 2>&1 &
|
|
1814
1822
|
disown
|
|
1815
1823
|
|
|
1816
|
-
#
|
|
1824
|
+
# wait for relay ready
|
|
1817
1825
|
local _i
|
|
1818
1826
|
for _i in {1..30}; do
|
|
1819
1827
|
(echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null && break
|
|
@@ -1821,7 +1829,7 @@ _relay_start() {
|
|
|
1821
1829
|
done
|
|
1822
1830
|
|
|
1823
1831
|
if ! (echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null; then
|
|
1824
|
-
echo "
|
|
1832
|
+
echo "error: relay startup timeout" >&2
|
|
1825
1833
|
return 1
|
|
1826
1834
|
fi
|
|
1827
1835
|
|
|
@@ -1835,7 +1843,7 @@ _relay_stop() {
|
|
|
1835
1843
|
local pid; pid=$(tr -d '[:space:]' < "$pid_file")
|
|
1836
1844
|
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
1837
1845
|
kill "$pid" 2>/dev/null
|
|
1838
|
-
#
|
|
1846
|
+
# wait for process exit
|
|
1839
1847
|
local _i
|
|
1840
1848
|
for _i in {1..20}; do
|
|
1841
1849
|
kill -0 "$pid" 2>/dev/null || break
|
|
@@ -1846,7 +1854,7 @@ _relay_stop() {
|
|
|
1846
1854
|
fi
|
|
1847
1855
|
rm -f "$CAC_DIR/relay.port"
|
|
1848
1856
|
|
|
1849
|
-
#
|
|
1857
|
+
# cleanup route
|
|
1850
1858
|
_relay_remove_route 2>/dev/null || true
|
|
1851
1859
|
}
|
|
1852
1860
|
|
|
@@ -1857,17 +1865,17 @@ _relay_is_running() {
|
|
|
1857
1865
|
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
|
|
1858
1866
|
}
|
|
1859
1867
|
|
|
1860
|
-
# ──
|
|
1868
|
+
# ── route management (direct route to bypass TUN) ──────────────────────────────
|
|
1861
1869
|
|
|
1862
1870
|
_relay_add_route() {
|
|
1863
1871
|
local proxy="$1"
|
|
1864
1872
|
local proxy_host; proxy_host=$(_proxy_host_port "$proxy")
|
|
1865
1873
|
proxy_host="${proxy_host%%:*}"
|
|
1866
1874
|
|
|
1867
|
-
#
|
|
1875
|
+
# skip loopback addresses
|
|
1868
1876
|
[[ "$proxy_host" == "127."* || "$proxy_host" == "localhost" ]] && return 0
|
|
1869
1877
|
|
|
1870
|
-
#
|
|
1878
|
+
# resolve to IP
|
|
1871
1879
|
local proxy_ip
|
|
1872
1880
|
proxy_ip=$(python3 -c "import socket; print(socket.gethostbyname('$proxy_host'))" 2>/dev/null || echo "$proxy_host")
|
|
1873
1881
|
|
|
@@ -1877,12 +1885,12 @@ _relay_add_route() {
|
|
|
1877
1885
|
gateway=$(route -n get default 2>/dev/null | awk '/gateway:/{print $2}')
|
|
1878
1886
|
[[ -z "$gateway" ]] && return 1
|
|
1879
1887
|
|
|
1880
|
-
#
|
|
1888
|
+
# check if direct route exists
|
|
1881
1889
|
local current_gw
|
|
1882
1890
|
current_gw=$(route -n get "$proxy_ip" 2>/dev/null | awk '/gateway:/{print $2}')
|
|
1883
1891
|
[[ "$current_gw" == "$gateway" ]] && return 0
|
|
1884
1892
|
|
|
1885
|
-
echo "
|
|
1893
|
+
echo " adding direct route: $proxy_ip -> $gateway (needs sudo)"
|
|
1886
1894
|
sudo route add -host "$proxy_ip" "$gateway" >/dev/null 2>&1 || return 1
|
|
1887
1895
|
echo "$proxy_ip" > "$CAC_DIR/relay_route_ip"
|
|
1888
1896
|
|
|
@@ -1892,7 +1900,7 @@ _relay_add_route() {
|
|
|
1892
1900
|
iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
|
1893
1901
|
[[ -z "$gateway" ]] && return 1
|
|
1894
1902
|
|
|
1895
|
-
echo "
|
|
1903
|
+
echo " adding direct route: $proxy_ip -> $gateway dev $iface (needs sudo)"
|
|
1896
1904
|
sudo ip route add "$proxy_ip/32" via "$gateway" dev "$iface" 2>/dev/null || return 1
|
|
1897
1905
|
echo "$proxy_ip" > "$CAC_DIR/relay_route_ip"
|
|
1898
1906
|
fi
|
|
@@ -1914,7 +1922,7 @@ _relay_remove_route() {
|
|
|
1914
1922
|
rm -f "$route_file"
|
|
1915
1923
|
}
|
|
1916
1924
|
|
|
1917
|
-
#
|
|
1925
|
+
# detect if TUN interface is active
|
|
1918
1926
|
_detect_tun_active() {
|
|
1919
1927
|
local os; os=$(_detect_os)
|
|
1920
1928
|
if [[ "$os" == "macos" ]]; then
|
|
@@ -1928,12 +1936,12 @@ _detect_tun_active() {
|
|
|
1928
1936
|
fi
|
|
1929
1937
|
}
|
|
1930
1938
|
|
|
1931
|
-
# ──
|
|
1939
|
+
# ── user commands ─────────────────────────────────────────────────────
|
|
1932
1940
|
|
|
1933
1941
|
cmd_relay() {
|
|
1934
1942
|
_require_setup
|
|
1935
1943
|
local current; current=$(_current_env)
|
|
1936
|
-
[[ -z "$current" ]] && { echo "
|
|
1944
|
+
[[ -z "$current" ]] && { echo "error: no active environment, run 'cac <name>' first" >&2; exit 1; }
|
|
1937
1945
|
|
|
1938
1946
|
local env_dir="$ENVS_DIR/$current"
|
|
1939
1947
|
local action="${1:-status}"
|
|
@@ -1942,60 +1950,60 @@ cmd_relay() {
|
|
|
1942
1950
|
case "$action" in
|
|
1943
1951
|
on)
|
|
1944
1952
|
echo "on" > "$env_dir/relay"
|
|
1945
|
-
echo "$(_green "✓") Relay
|
|
1953
|
+
echo "$(_green "✓") Relay enabled (env: $(_bold "$current"))"
|
|
1946
1954
|
|
|
1947
|
-
# --route
|
|
1955
|
+
# --route flag: add direct route
|
|
1948
1956
|
if [[ "$flag" == "--route" ]]; then
|
|
1949
1957
|
local proxy; proxy=$(_read "$env_dir/proxy")
|
|
1950
1958
|
_relay_add_route "$proxy"
|
|
1951
1959
|
fi
|
|
1952
1960
|
|
|
1953
|
-
#
|
|
1961
|
+
# start relay if not running
|
|
1954
1962
|
if ! _relay_is_running; then
|
|
1955
|
-
printf "
|
|
1963
|
+
printf " starting relay ... "
|
|
1956
1964
|
if _relay_start "$current"; then
|
|
1957
1965
|
local port; port=$(_read "$CAC_DIR/relay.port")
|
|
1958
1966
|
echo "$(_green "✓") 127.0.0.1:$port"
|
|
1959
1967
|
else
|
|
1960
|
-
echo "$(_red "✗
|
|
1968
|
+
echo "$(_red "✗ failed to start")"
|
|
1961
1969
|
fi
|
|
1962
1970
|
fi
|
|
1963
|
-
echo "
|
|
1971
|
+
echo " next claude launch will automatically connect via local relay"
|
|
1964
1972
|
;;
|
|
1965
1973
|
off)
|
|
1966
1974
|
rm -f "$env_dir/relay"
|
|
1967
1975
|
_relay_stop
|
|
1968
|
-
echo "$(_green "✓") Relay
|
|
1976
|
+
echo "$(_green "✓") Relay disabled (env: $(_bold "$current"))"
|
|
1969
1977
|
;;
|
|
1970
1978
|
status)
|
|
1971
1979
|
if [[ -f "$env_dir/relay" ]] && [[ "$(_read "$env_dir/relay")" == "on" ]]; then
|
|
1972
|
-
echo "Relay
|
|
1980
|
+
echo "Relay mode: $(_green "enabled")"
|
|
1973
1981
|
else
|
|
1974
|
-
echo "Relay
|
|
1982
|
+
echo "Relay mode: disabled"
|
|
1975
1983
|
if _detect_tun_active; then
|
|
1976
|
-
echo " $(_yellow "⚠")
|
|
1984
|
+
echo " $(_yellow "⚠") TUN mode detected, consider running 'cac relay on'"
|
|
1977
1985
|
fi
|
|
1978
1986
|
return
|
|
1979
1987
|
fi
|
|
1980
1988
|
|
|
1981
1989
|
if _relay_is_running; then
|
|
1982
1990
|
local pid; pid=$(_read "$CAC_DIR/relay.pid")
|
|
1983
|
-
local port; port=$(_read "$CAC_DIR/relay.port" "
|
|
1984
|
-
echo "Relay
|
|
1991
|
+
local port; port=$(_read "$CAC_DIR/relay.port" "unknown")
|
|
1992
|
+
echo "Relay process: $(_green "running") (PID=$pid, port=$port)"
|
|
1985
1993
|
else
|
|
1986
|
-
echo "Relay
|
|
1994
|
+
echo "Relay process: $(_yellow "not started") (will auto-start with claude)"
|
|
1987
1995
|
fi
|
|
1988
1996
|
|
|
1989
1997
|
if [[ -f "$CAC_DIR/relay_route_ip" ]]; then
|
|
1990
1998
|
local route_ip; route_ip=$(_read "$CAC_DIR/relay_route_ip")
|
|
1991
|
-
echo "
|
|
1999
|
+
echo "Direct route: $route_ip"
|
|
1992
2000
|
fi
|
|
1993
2001
|
;;
|
|
1994
2002
|
*)
|
|
1995
|
-
echo "
|
|
1996
|
-
echo " on [--route]
|
|
1997
|
-
echo " off
|
|
1998
|
-
echo " status
|
|
2003
|
+
echo "usage: cac relay [on|off|status]" >&2
|
|
2004
|
+
echo " on [--route] enable local relay (--route adds direct route to bypass TUN)" >&2
|
|
2005
|
+
echo " off disable local relay" >&2
|
|
2006
|
+
echo " status show status" >&2
|
|
1999
2007
|
;;
|
|
2000
2008
|
esac
|
|
2001
2009
|
}
|
|
@@ -2199,17 +2207,17 @@ cmd_stop() {
|
|
|
2199
2207
|
|
|
2200
2208
|
cmd_continue() {
|
|
2201
2209
|
if [[ ! -f "$CAC_DIR/stopped" ]]; then
|
|
2202
|
-
echo "cac
|
|
2210
|
+
echo "cac is not stopped, no need to resume"
|
|
2203
2211
|
return
|
|
2204
2212
|
fi
|
|
2205
2213
|
|
|
2206
2214
|
local current; current=$(_current_env)
|
|
2207
2215
|
if [[ -z "$current" ]]; then
|
|
2208
|
-
echo "
|
|
2216
|
+
echo "error: no active environment, run 'cac <name>'" >&2; exit 1
|
|
2209
2217
|
fi
|
|
2210
2218
|
|
|
2211
2219
|
rm -f "$CAC_DIR/stopped"
|
|
2212
|
-
echo "$(_green "✓") cac
|
|
2220
|
+
echo "$(_green "✓") cac resumed — current env: $(_bold "$current")"
|
|
2213
2221
|
}
|
|
2214
2222
|
|
|
2215
2223
|
# ━━━ cmd_claude.sh ━━━
|
|
@@ -2939,7 +2947,7 @@ cmd_docker() {
|
|
|
2939
2947
|
}
|
|
2940
2948
|
|
|
2941
2949
|
# ━━━ cmd_delete.sh ━━━
|
|
2942
|
-
# ── cmd: delete
|
|
2950
|
+
# ── cmd: delete (uninstall) ────────────────────────────────────────
|
|
2943
2951
|
|
|
2944
2952
|
cmd_delete() {
|
|
2945
2953
|
echo "=== cac delete ==="
|
|
@@ -2950,50 +2958,50 @@ cmd_delete() {
|
|
|
2950
2958
|
|
|
2951
2959
|
_remove_path_from_rc "$rc_file"
|
|
2952
2960
|
|
|
2953
|
-
#
|
|
2961
|
+
# stop relay processes and routes
|
|
2954
2962
|
if [[ -d "$CAC_DIR" ]]; then
|
|
2955
2963
|
_relay_stop 2>/dev/null || true
|
|
2956
2964
|
|
|
2957
|
-
#
|
|
2965
|
+
# stop docker port-forward processes
|
|
2958
2966
|
if [[ -d /tmp/cac-docker-ports ]]; then
|
|
2959
2967
|
for _pf in /tmp/cac-docker-ports/*.pid; do
|
|
2960
2968
|
[[ -f "$_pf" ]] || continue
|
|
2961
2969
|
kill "$(cat "$_pf")" 2>/dev/null || true
|
|
2962
2970
|
rm -f "$_pf"
|
|
2963
2971
|
done
|
|
2964
|
-
echo " ✓
|
|
2972
|
+
echo " ✓ stopped docker port-forward processes"
|
|
2965
2973
|
fi
|
|
2966
2974
|
|
|
2967
|
-
#
|
|
2975
|
+
# fallback: clean up orphaned relay processes
|
|
2968
2976
|
pkill -f "node.*\.cac/relay\.js" 2>/dev/null || true
|
|
2969
2977
|
|
|
2970
2978
|
rm -rf "$CAC_DIR"
|
|
2971
|
-
echo " ✓
|
|
2979
|
+
echo " ✓ deleted $CAC_DIR"
|
|
2972
2980
|
else
|
|
2973
|
-
echo " - $CAC_DIR
|
|
2981
|
+
echo " - $CAC_DIR does not exist, skipping"
|
|
2974
2982
|
fi
|
|
2975
2983
|
|
|
2976
2984
|
local method
|
|
2977
2985
|
method=$(_install_method)
|
|
2978
2986
|
echo
|
|
2979
2987
|
if [[ "$method" == "npm" ]]; then
|
|
2980
|
-
echo " ✓
|
|
2988
|
+
echo " ✓ cleared all cac data and config"
|
|
2981
2989
|
echo
|
|
2982
|
-
echo "
|
|
2990
|
+
echo "to fully uninstall the cac command, run:"
|
|
2983
2991
|
echo " npm uninstall -g claude-cac"
|
|
2984
2992
|
else
|
|
2985
2993
|
if [[ -f "$HOME/bin/cac" ]]; then
|
|
2986
2994
|
rm -f "$HOME/bin/cac"
|
|
2987
|
-
echo " ✓
|
|
2995
|
+
echo " ✓ deleted $HOME/bin/cac"
|
|
2988
2996
|
fi
|
|
2989
|
-
echo " ✓
|
|
2997
|
+
echo " ✓ uninstall complete"
|
|
2990
2998
|
fi
|
|
2991
2999
|
|
|
2992
3000
|
echo
|
|
2993
3001
|
if [[ -n "$rc_file" ]]; then
|
|
2994
|
-
echo "
|
|
3002
|
+
echo "please restart terminal or run source $rc_file for changes to take effect."
|
|
2995
3003
|
else
|
|
2996
|
-
echo "
|
|
3004
|
+
echo "please restart terminal for changes to take effect."
|
|
2997
3005
|
fi
|
|
2998
3006
|
}
|
|
2999
3007
|
|
|
@@ -3045,7 +3053,7 @@ cmd_help() {
|
|
|
3045
3053
|
}
|
|
3046
3054
|
|
|
3047
3055
|
# ━━━ main.sh ━━━
|
|
3048
|
-
# ──
|
|
3056
|
+
# ── entry: dispatch commands ──────────────────────────────────────────────
|
|
3049
3057
|
|
|
3050
3058
|
[[ $# -eq 0 ]] && { cmd_help; exit 0; }
|
|
3051
3059
|
|