claude-cac 1.3.0 → 1.3.2
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/cac +191 -188
- package/package.json +1 -1
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.2"
|
|
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
|
|
@@ -322,18 +322,21 @@ _write_path_to_rc() {
|
|
|
322
322
|
cat >> "$rc_file" << 'CACEOF'
|
|
323
323
|
|
|
324
324
|
# >>> cac — Claude Code Cloak >>>
|
|
325
|
-
PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '\.cac/bin' |
|
|
326
|
-
export PATH="$HOME/.cac/bin:$
|
|
325
|
+
PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '\.cac/bin' | tr '\n' ':' | sed 's/:$//')
|
|
326
|
+
export PATH="$HOME/.cac/bin:$PATH"
|
|
327
327
|
cac() {
|
|
328
|
-
|
|
328
|
+
local _cac_bin
|
|
329
|
+
_cac_bin=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '\.cac/bin' | tr '\n' ':') command -v cac 2>/dev/null)
|
|
330
|
+
[[ -z "$_cac_bin" ]] && { echo "[cac] error: cac binary not found in PATH" >&2; return 1; }
|
|
331
|
+
command "$_cac_bin" "$@"
|
|
329
332
|
local _rc=$?
|
|
330
333
|
PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '\.cac/bin' | tr '\n' ':' | sed 's/:$//')
|
|
331
|
-
export PATH="$HOME/.cac/bin:$
|
|
334
|
+
export PATH="$HOME/.cac/bin:$PATH"
|
|
332
335
|
return $_rc
|
|
333
336
|
}
|
|
334
337
|
# <<< cac — Claude Code Cloak <<<
|
|
335
338
|
CACEOF
|
|
336
|
-
echo " ✓ PATH
|
|
339
|
+
echo " ✓ PATH written to $rc_file"
|
|
337
340
|
return 0
|
|
338
341
|
}
|
|
339
342
|
|
|
@@ -341,23 +344,23 @@ _remove_path_from_rc() {
|
|
|
341
344
|
local rc_file="${1:-$(_detect_rc_file)}"
|
|
342
345
|
[[ -z "$rc_file" ]] && return 0
|
|
343
346
|
|
|
344
|
-
#
|
|
347
|
+
# Remove marked block (new format)
|
|
345
348
|
if grep -q '# >>> cac' "$rc_file" 2>/dev/null; then
|
|
346
349
|
local tmp="${rc_file}.cac-tmp"
|
|
347
350
|
awk '/# >>> cac/{skip=1; next} /# <<< cac/{skip=0; next} !skip' "$rc_file" > "$tmp"
|
|
348
351
|
cat -s "$tmp" > "$rc_file"
|
|
349
352
|
rm -f "$tmp"
|
|
350
|
-
echo " ✓
|
|
353
|
+
echo " ✓ Removed PATH config from $rc_file"
|
|
351
354
|
return 0
|
|
352
355
|
fi
|
|
353
356
|
|
|
354
|
-
#
|
|
357
|
+
# Compat: old format
|
|
355
358
|
if grep -qE '(\.cac/bin|# cac —)' "$rc_file" 2>/dev/null; then
|
|
356
359
|
local tmp="${rc_file}.cac-tmp"
|
|
357
360
|
grep -vE '(# cac — Claude Code Cloak|\.cac/bin|# cac 命令|# claude wrapper)' "$rc_file" > "$tmp" || true
|
|
358
361
|
cat -s "$tmp" > "$rc_file"
|
|
359
362
|
rm -f "$tmp"
|
|
360
|
-
echo " ✓
|
|
363
|
+
echo " ✓ Removed PATH config from $rc_file (old format)"
|
|
361
364
|
return 0
|
|
362
365
|
fi
|
|
363
366
|
}
|
|
@@ -400,9 +403,9 @@ PYEOF
|
|
|
400
403
|
}
|
|
401
404
|
|
|
402
405
|
# ━━━ dns_block.sh ━━━
|
|
403
|
-
# ── DNS
|
|
406
|
+
# ── DNS interception & telemetry domain blocking ─────────────────────────────────────
|
|
404
407
|
|
|
405
|
-
#
|
|
408
|
+
# telemetry domains to block
|
|
406
409
|
TELEMETRY_DOMAINS=(
|
|
407
410
|
"statsig.anthropic.com"
|
|
408
411
|
"sentry.io"
|
|
@@ -410,8 +413,8 @@ TELEMETRY_DOMAINS=(
|
|
|
410
413
|
"cdn.growthbook.io"
|
|
411
414
|
)
|
|
412
415
|
|
|
413
|
-
#
|
|
414
|
-
# HOSTALIASES
|
|
416
|
+
# write HOSTALIASES file (fallback layer: gethostbyname-level blocking)
|
|
417
|
+
# HOSTALIASES format is hostname-to-hostname mapping (not IP)
|
|
415
418
|
_write_blocked_hosts() {
|
|
416
419
|
local hosts_file="$CAC_DIR/blocked_hosts"
|
|
417
420
|
{
|
|
@@ -423,14 +426,14 @@ _write_blocked_hosts() {
|
|
|
423
426
|
} > "$hosts_file"
|
|
424
427
|
}
|
|
425
428
|
|
|
426
|
-
#
|
|
429
|
+
# write Node.js DNS guard module (core layer: dns.lookup / dns.resolve level blocking + mTLS)
|
|
427
430
|
_write_dns_guard_js() {
|
|
428
431
|
local guard_file="$CAC_DIR/cac-dns-guard.js"
|
|
429
432
|
cat > "$guard_file" << 'DNSGUARD_EOF'
|
|
430
433
|
// ═══════════════════════════════════════════════════════════════
|
|
431
434
|
// cac-dns-guard.js
|
|
432
|
-
//
|
|
433
|
-
//
|
|
435
|
+
// DNS-level telemetry domain blocking | mTLS certificate injection | fetch leak prevention
|
|
436
|
+
// Injected into Claude Code process via NODE_OPTIONS="--require <this>"
|
|
434
437
|
// ═══════════════════════════════════════════════════════════════
|
|
435
438
|
'use strict';
|
|
436
439
|
|
|
@@ -441,7 +444,7 @@ var http = require('http');
|
|
|
441
444
|
var https = require('https');
|
|
442
445
|
var fs = require('fs');
|
|
443
446
|
|
|
444
|
-
// ─── 1.
|
|
447
|
+
// ─── 1. DNS-level telemetry domain blocking ──────────────────────────────────
|
|
445
448
|
|
|
446
449
|
var BLOCKED_DOMAINS = new Set([
|
|
447
450
|
'statsig.anthropic.com',
|
|
@@ -451,8 +454,8 @@ var BLOCKED_DOMAINS = new Set([
|
|
|
451
454
|
]);
|
|
452
455
|
|
|
453
456
|
/**
|
|
454
|
-
*
|
|
455
|
-
* 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"
|
|
456
459
|
*/
|
|
457
460
|
function isDomainBlocked(hostname) {
|
|
458
461
|
if (!hostname) return false;
|
|
@@ -475,7 +478,7 @@ function makeBlockedError(hostname, syscall) {
|
|
|
475
478
|
return err;
|
|
476
479
|
}
|
|
477
480
|
|
|
478
|
-
// ── 1a.
|
|
481
|
+
// ── 1a. intercept dns.lookup ──
|
|
479
482
|
var _origLookup = dns.lookup;
|
|
480
483
|
dns.lookup = function cacLookup(hostname, options, callback) {
|
|
481
484
|
if (typeof options === 'function') { callback = options; options = {}; }
|
|
@@ -487,7 +490,7 @@ dns.lookup = function cacLookup(hostname, options, callback) {
|
|
|
487
490
|
return _origLookup.call(dns, hostname, options, callback);
|
|
488
491
|
};
|
|
489
492
|
|
|
490
|
-
// ── 1b.
|
|
493
|
+
// ── 1b. intercept dns.resolve / resolve4 / resolve6 ──
|
|
491
494
|
['resolve','resolve4','resolve6'].forEach(function(method) {
|
|
492
495
|
var orig = dns[method];
|
|
493
496
|
if (!orig) return;
|
|
@@ -503,7 +506,7 @@ dns.lookup = function cacLookup(hostname, options, callback) {
|
|
|
503
506
|
};
|
|
504
507
|
});
|
|
505
508
|
|
|
506
|
-
// ── 1c.
|
|
509
|
+
// ── 1c. intercept dns.promises ──
|
|
507
510
|
if (dns.promises) {
|
|
508
511
|
var _origPLookup = dns.promises.lookup;
|
|
509
512
|
if (_origPLookup) {
|
|
@@ -522,7 +525,7 @@ if (dns.promises) {
|
|
|
522
525
|
});
|
|
523
526
|
}
|
|
524
527
|
|
|
525
|
-
// ── 1d.
|
|
528
|
+
// ── 1d. network layer safety net: intercept net.connect to telemetry domains ──
|
|
526
529
|
var _origNetConnect = net.connect;
|
|
527
530
|
var _origNetCreateConn = net.createConnection;
|
|
528
531
|
|
|
@@ -550,7 +553,7 @@ net.createConnection = function cacNetCreateConnection() {
|
|
|
550
553
|
};
|
|
551
554
|
|
|
552
555
|
|
|
553
|
-
// ─── 2. mTLS
|
|
556
|
+
// ─── 2. mTLS certificate injection ──────────────────────────────────
|
|
554
557
|
|
|
555
558
|
var mtlsCert = process.env.CAC_MTLS_CERT;
|
|
556
559
|
var mtlsKey = process.env.CAC_MTLS_KEY;
|
|
@@ -568,14 +571,14 @@ if (mtlsCert && mtlsKey) {
|
|
|
568
571
|
}
|
|
569
572
|
|
|
570
573
|
if (certData && keyData) {
|
|
571
|
-
//
|
|
574
|
+
// inject mTLS cert only for proxy connections
|
|
572
575
|
var proxyHost = proxyHostPort.split(':')[0];
|
|
573
576
|
var proxyPort = parseInt(proxyHostPort.split(':')[1], 10) || 0;
|
|
574
577
|
|
|
575
|
-
// 2a.
|
|
578
|
+
// 2a. intercept tls.connect, inject client cert for proxy connections
|
|
576
579
|
var _origTlsConnect = tls.connect;
|
|
577
580
|
tls.connect = function cacTlsConnect() {
|
|
578
|
-
//
|
|
581
|
+
// normalize parameters: tls.connect(options[, cb]) or tls.connect(port[, host][, options][, cb])
|
|
579
582
|
var args = Array.prototype.slice.call(arguments);
|
|
580
583
|
var options, callback;
|
|
581
584
|
|
|
@@ -583,7 +586,7 @@ if (mtlsCert && mtlsKey) {
|
|
|
583
586
|
options = args[0];
|
|
584
587
|
callback = (typeof args[1] === 'function') ? args[1] : undefined;
|
|
585
588
|
} else {
|
|
586
|
-
// tls.connect(port, host, options, cb)
|
|
589
|
+
// tls.connect(port, host, options, cb) form
|
|
587
590
|
var port = args[0];
|
|
588
591
|
var host = (typeof args[1] === 'string') ? args[1] : 'localhost';
|
|
589
592
|
var optIdx = (typeof args[1] === 'string') ? 2 : 1;
|
|
@@ -594,7 +597,7 @@ if (mtlsCert && mtlsKey) {
|
|
|
594
597
|
if (typeof callback !== 'function') callback = undefined;
|
|
595
598
|
}
|
|
596
599
|
|
|
597
|
-
//
|
|
600
|
+
// inject only for proxy connections (exact host:port match)
|
|
598
601
|
var targetHost = options.host || options.hostname || '';
|
|
599
602
|
var targetPort = options.port || 0;
|
|
600
603
|
if (proxyHost && targetHost === proxyHost &&
|
|
@@ -614,7 +617,7 @@ if (mtlsCert && mtlsKey) {
|
|
|
614
617
|
return _origTlsConnect.call(tls, options);
|
|
615
618
|
};
|
|
616
619
|
|
|
617
|
-
// 2b.
|
|
620
|
+
// 2b. inject CA into https.globalAgent (trust CA only, no client private key)
|
|
618
621
|
if (caData && https.globalAgent && https.globalAgent.options) {
|
|
619
622
|
https.globalAgent.options.ca = https.globalAgent.options.ca
|
|
620
623
|
? [].concat(https.globalAgent.options.ca, caData)
|
|
@@ -624,15 +627,15 @@ if (mtlsCert && mtlsKey) {
|
|
|
624
627
|
}
|
|
625
628
|
|
|
626
629
|
|
|
627
|
-
// ─── 3. fetch
|
|
628
|
-
// Node.js
|
|
629
|
-
//
|
|
630
|
-
//
|
|
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
|
|
631
634
|
|
|
632
635
|
(function patchFetch() {
|
|
633
636
|
if (typeof globalThis === 'undefined') return;
|
|
634
637
|
|
|
635
|
-
//
|
|
638
|
+
// prefer node-fetch (based on http/https modules, naturally goes through dns.lookup interception chain)
|
|
636
639
|
try {
|
|
637
640
|
var nodeFetch = require('node-fetch');
|
|
638
641
|
if (nodeFetch && typeof nodeFetch === 'function') {
|
|
@@ -643,10 +646,10 @@ if (mtlsCert && mtlsKey) {
|
|
|
643
646
|
return;
|
|
644
647
|
}
|
|
645
648
|
} catch(e) {
|
|
646
|
-
// node-fetch
|
|
649
|
+
// node-fetch not available
|
|
647
650
|
}
|
|
648
651
|
|
|
649
|
-
//
|
|
652
|
+
// fallback: wrap native fetch to ensure telemetry domains are blocked
|
|
650
653
|
if (typeof globalThis.fetch === 'function') {
|
|
651
654
|
var _origFetch = globalThis.fetch;
|
|
652
655
|
globalThis.fetch = function cacFetch(input, init) {
|
|
@@ -666,15 +669,15 @@ if (mtlsCert && mtlsKey) {
|
|
|
666
669
|
})();
|
|
667
670
|
|
|
668
671
|
|
|
669
|
-
// ─── 4.
|
|
670
|
-
// Claude Code
|
|
671
|
-
// Cloudflare
|
|
672
|
-
//
|
|
673
|
-
//
|
|
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
|
|
674
677
|
|
|
675
678
|
function isHealthCheck(url) {
|
|
676
679
|
if (!url) return false;
|
|
677
|
-
//
|
|
680
|
+
// match https://api.anthropic.com/api/hello or variants with query params
|
|
678
681
|
return /^https?:\/\/api\.anthropic\.com\/api\/hello/.test(url);
|
|
679
682
|
}
|
|
680
683
|
|
|
@@ -714,7 +717,7 @@ function makeHealthResponse(callback) {
|
|
|
714
717
|
return req;
|
|
715
718
|
}
|
|
716
719
|
|
|
717
|
-
// 4a.
|
|
720
|
+
// 4a. intercept https.get / https.request
|
|
718
721
|
var _origHttpsRequest = https.request;
|
|
719
722
|
var _origHttpsGet = https.get;
|
|
720
723
|
|
|
@@ -752,7 +755,7 @@ https.get = function cacHttpsGet(urlOrOpts, optsOrCb, cb) {
|
|
|
752
755
|
return _origHttpsGet.apply(https, arguments);
|
|
753
756
|
};
|
|
754
757
|
|
|
755
|
-
// 4b.
|
|
758
|
+
// 4b. intercept fetch (undici bypasses https module)
|
|
756
759
|
(function patchHealthFetch() {
|
|
757
760
|
if (typeof globalThis === 'undefined' || typeof globalThis.fetch !== 'function') return;
|
|
758
761
|
var _prevFetch = globalThis.fetch;
|
|
@@ -774,13 +777,13 @@ DNSGUARD_EOF
|
|
|
774
777
|
chmod 644 "$guard_file"
|
|
775
778
|
}
|
|
776
779
|
|
|
777
|
-
#
|
|
780
|
+
# verify DNS blocking is active
|
|
778
781
|
_check_dns_block() {
|
|
779
782
|
local domain="${1:-statsig.anthropic.com}"
|
|
780
783
|
local guard_file="$CAC_DIR/cac-dns-guard.js"
|
|
781
784
|
|
|
782
785
|
if [[ ! -f "$guard_file" ]]; then
|
|
783
|
-
echo "$(_red "✗") DNS guard
|
|
786
|
+
echo "$(_red "✗") DNS guard module not found"
|
|
784
787
|
return 1
|
|
785
788
|
fi
|
|
786
789
|
|
|
@@ -794,37 +797,37 @@ _check_dns_block() {
|
|
|
794
797
|
' "$guard_file" "$domain" 2>/dev/null || echo "ERROR")
|
|
795
798
|
|
|
796
799
|
if [[ "$result" == "BLOCKED" ]]; then
|
|
797
|
-
echo "$(_green "✓") $domain
|
|
800
|
+
echo "$(_green "✓") $domain blocked"
|
|
798
801
|
return 0
|
|
799
802
|
else
|
|
800
|
-
echo "$(_red "✗") $domain
|
|
803
|
+
echo "$(_red "✗") $domain not blocked ($result)"
|
|
801
804
|
return 1
|
|
802
805
|
fi
|
|
803
806
|
}
|
|
804
807
|
|
|
805
808
|
# ━━━ mtls.sh ━━━
|
|
806
|
-
# ── mTLS
|
|
809
|
+
# ── mTLS client certificate management ─────────────────────────────────────────
|
|
807
810
|
|
|
808
|
-
#
|
|
811
|
+
# generate self-signed CA (called during setup, generated only once)
|
|
809
812
|
_generate_ca_cert() {
|
|
810
813
|
local ca_dir="$CAC_DIR/ca"
|
|
811
814
|
local ca_key="$ca_dir/ca_key.pem"
|
|
812
815
|
local ca_cert="$ca_dir/ca_cert.pem"
|
|
813
816
|
|
|
814
817
|
if [[ -f "$ca_cert" ]] && [[ -f "$ca_key" ]]; then
|
|
815
|
-
echo " CA
|
|
818
|
+
echo " CA cert exists, skipping"
|
|
816
819
|
return 0
|
|
817
820
|
fi
|
|
818
821
|
|
|
819
822
|
mkdir -p "$ca_dir"
|
|
820
823
|
|
|
821
|
-
#
|
|
824
|
+
# generate CA private key (4096-bit RSA)
|
|
822
825
|
openssl genrsa -out "$ca_key" 4096 2>/dev/null || {
|
|
823
|
-
echo "
|
|
826
|
+
echo "error: failed to generate CA private key" >&2; return 1
|
|
824
827
|
}
|
|
825
828
|
chmod 600 "$ca_key"
|
|
826
829
|
|
|
827
|
-
#
|
|
830
|
+
# generate self-signed CA cert (valid for 10 years)
|
|
828
831
|
openssl req -new -x509 \
|
|
829
832
|
-key "$ca_key" \
|
|
830
833
|
-out "$ca_cert" \
|
|
@@ -833,12 +836,12 @@ _generate_ca_cert() {
|
|
|
833
836
|
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
|
|
834
837
|
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
|
835
838
|
2>/dev/null || {
|
|
836
|
-
echo "
|
|
839
|
+
echo "error: failed to generate CA cert" >&2; return 1
|
|
837
840
|
}
|
|
838
841
|
chmod 644 "$ca_cert"
|
|
839
842
|
}
|
|
840
843
|
|
|
841
|
-
#
|
|
844
|
+
# generate client cert for environment (called during cac add)
|
|
842
845
|
_generate_client_cert() {
|
|
843
846
|
local name="$1"
|
|
844
847
|
local env_dir="$ENVS_DIR/$name"
|
|
@@ -846,7 +849,7 @@ _generate_client_cert() {
|
|
|
846
849
|
local ca_cert="$CAC_DIR/ca/ca_cert.pem"
|
|
847
850
|
|
|
848
851
|
if [[ ! -f "$ca_key" ]] || [[ ! -f "$ca_cert" ]]; then
|
|
849
|
-
echo "
|
|
852
|
+
echo " warning: CA cert not found, skipping client cert generation" >&2
|
|
850
853
|
return 1
|
|
851
854
|
fi
|
|
852
855
|
|
|
@@ -854,22 +857,22 @@ _generate_client_cert() {
|
|
|
854
857
|
local client_csr="$env_dir/client_csr.pem"
|
|
855
858
|
local client_cert="$env_dir/client_cert.pem"
|
|
856
859
|
|
|
857
|
-
#
|
|
860
|
+
# generate client private key (2048-bit RSA)
|
|
858
861
|
openssl genrsa -out "$client_key" 2048 2>/dev/null || {
|
|
859
|
-
echo "
|
|
862
|
+
echo "error: failed to generate client private key" >&2; return 1
|
|
860
863
|
}
|
|
861
864
|
chmod 600 "$client_key"
|
|
862
865
|
|
|
863
|
-
#
|
|
866
|
+
# generate CSR
|
|
864
867
|
openssl req -new \
|
|
865
868
|
-key "$client_key" \
|
|
866
869
|
-out "$client_csr" \
|
|
867
870
|
-subj "/CN=cac-client-${name}/O=cac/OU=env-${name}" \
|
|
868
871
|
2>/dev/null || {
|
|
869
|
-
echo "
|
|
872
|
+
echo "error: failed to generate CSR" >&2; return 1
|
|
870
873
|
}
|
|
871
874
|
|
|
872
|
-
#
|
|
875
|
+
# sign client cert with CA (valid for 1 year)
|
|
873
876
|
openssl x509 -req \
|
|
874
877
|
-in "$client_csr" \
|
|
875
878
|
-CA "$ca_cert" \
|
|
@@ -879,52 +882,52 @@ _generate_client_cert() {
|
|
|
879
882
|
-days 365 \
|
|
880
883
|
-extfile <(printf "keyUsage=critical,digitalSignature\nextendedKeyUsage=clientAuth") \
|
|
881
884
|
2>/dev/null || {
|
|
882
|
-
echo "
|
|
885
|
+
echo "error: failed to sign client cert" >&2; return 1
|
|
883
886
|
}
|
|
884
887
|
chmod 644 "$client_cert"
|
|
885
888
|
|
|
886
|
-
#
|
|
889
|
+
# cleanup CSR (no longer needed)
|
|
887
890
|
rm -f "$client_csr"
|
|
888
891
|
}
|
|
889
892
|
|
|
890
|
-
#
|
|
893
|
+
# verify mTLS certificate status
|
|
891
894
|
_check_mtls() {
|
|
892
895
|
local env_dir="$1"
|
|
893
896
|
local ca_cert="$CAC_DIR/ca/ca_cert.pem"
|
|
894
897
|
local client_cert="$env_dir/client_cert.pem"
|
|
895
898
|
local client_key="$env_dir/client_key.pem"
|
|
896
899
|
|
|
897
|
-
#
|
|
900
|
+
# check CA
|
|
898
901
|
if [[ ! -f "$ca_cert" ]]; then
|
|
899
|
-
echo "$(_red "✗") CA
|
|
902
|
+
echo "$(_red "✗") CA cert not found"
|
|
900
903
|
return 1
|
|
901
904
|
fi
|
|
902
905
|
|
|
903
|
-
#
|
|
906
|
+
# check client cert
|
|
904
907
|
if [[ ! -f "$client_cert" ]] || [[ ! -f "$client_key" ]]; then
|
|
905
|
-
echo "$(_yellow "⚠")
|
|
908
|
+
echo "$(_yellow "⚠") client cert not found"
|
|
906
909
|
return 1
|
|
907
910
|
fi
|
|
908
911
|
|
|
909
|
-
#
|
|
912
|
+
# verify certificate chain
|
|
910
913
|
if openssl verify -CAfile "$ca_cert" "$client_cert" >/dev/null 2>&1; then
|
|
911
|
-
#
|
|
914
|
+
# check certificate expiry
|
|
912
915
|
local expiry
|
|
913
916
|
expiry=$(openssl x509 -in "$client_cert" -noout -enddate 2>/dev/null | cut -d= -f2)
|
|
914
917
|
local cn
|
|
915
918
|
cn=$(openssl x509 -in "$client_cert" -noout -subject 2>/dev/null | sed 's/.*CN *= *//')
|
|
916
|
-
echo "$(_green "✓") mTLS
|
|
919
|
+
echo "$(_green "✓") mTLS certificate valid (CN=$cn, expires: $expiry)"
|
|
917
920
|
return 0
|
|
918
921
|
else
|
|
919
|
-
echo "$(_red "✗")
|
|
922
|
+
echo "$(_red "✗") certificate chain verification failed"
|
|
920
923
|
return 1
|
|
921
924
|
fi
|
|
922
925
|
}
|
|
923
926
|
|
|
924
927
|
# ━━━ templates.sh ━━━
|
|
925
|
-
# ── templates:
|
|
928
|
+
# ── templates: wrapper, shim, env init ──────────────────
|
|
926
929
|
|
|
927
|
-
#
|
|
930
|
+
# write statusline-command.sh to env .claude dir
|
|
928
931
|
_write_statusline_script() {
|
|
929
932
|
local config_dir="$1"
|
|
930
933
|
cat > "$config_dir/statusline-command.sh" << 'STATUSLINE_EOF'
|
|
@@ -1017,7 +1020,7 @@ STATUSLINE_EOF
|
|
|
1017
1020
|
chmod +x "$config_dir/statusline-command.sh"
|
|
1018
1021
|
}
|
|
1019
1022
|
|
|
1020
|
-
#
|
|
1023
|
+
# write settings.json to env .claude dir
|
|
1021
1024
|
_write_env_settings() {
|
|
1022
1025
|
local config_dir="$1"
|
|
1023
1026
|
cat > "$config_dir/settings.json" << 'SETTINGS_EOF'
|
|
@@ -1034,7 +1037,7 @@ _write_env_settings() {
|
|
|
1034
1037
|
SETTINGS_EOF
|
|
1035
1038
|
}
|
|
1036
1039
|
|
|
1037
|
-
#
|
|
1040
|
+
# write CLAUDE.md to env .claude dir
|
|
1038
1041
|
_write_env_claude_md() {
|
|
1039
1042
|
local config_dir="$1"
|
|
1040
1043
|
local env_name="$2"
|
|
@@ -1063,25 +1066,25 @@ set -euo pipefail
|
|
|
1063
1066
|
CAC_DIR="$HOME/.cac"
|
|
1064
1067
|
ENVS_DIR="$CAC_DIR/envs"
|
|
1065
1068
|
|
|
1066
|
-
# cacstop
|
|
1069
|
+
# cacstop state: passthrough directly
|
|
1067
1070
|
if [[ -f "$CAC_DIR/stopped" ]]; then
|
|
1068
1071
|
_real=$(tr -d '[:space:]' < "$CAC_DIR/real_claude" 2>/dev/null || true)
|
|
1069
1072
|
[[ -x "$_real" ]] && exec "$_real" "$@"
|
|
1070
|
-
echo "[cac]
|
|
1073
|
+
echo "[cac] error: real claude not found, run 'cac setup'" >&2; exit 1
|
|
1071
1074
|
fi
|
|
1072
1075
|
|
|
1073
|
-
#
|
|
1076
|
+
# read current environment
|
|
1074
1077
|
if [[ ! -f "$CAC_DIR/current" ]]; then
|
|
1075
|
-
echo "[cac]
|
|
1078
|
+
echo "[cac] error: no active environment, run 'cac <name>'" >&2; exit 1
|
|
1076
1079
|
fi
|
|
1077
1080
|
_name=$(tr -d '[:space:]' < "$CAC_DIR/current")
|
|
1078
1081
|
_env_dir="$ENVS_DIR/$_name"
|
|
1079
|
-
[[ -d "$_env_dir" ]] || { echo "[cac]
|
|
1082
|
+
[[ -d "$_env_dir" ]] || { echo "[cac] error: environment '$_name' not found" >&2; exit 1; }
|
|
1080
1083
|
|
|
1081
1084
|
# Isolated .claude config directory
|
|
1082
1085
|
if [[ -d "$_env_dir/.claude" ]]; then
|
|
1083
1086
|
export CLAUDE_CONFIG_DIR="$_env_dir/.claude"
|
|
1084
|
-
#
|
|
1087
|
+
# ensure settings.json exists, prevent Claude Code fallback to ~/.claude/settings.json
|
|
1085
1088
|
[[ -f "$_env_dir/.claude/settings.json" ]] || echo '{}' > "$_env_dir/.claude/settings.json"
|
|
1086
1089
|
fi
|
|
1087
1090
|
|
|
@@ -1092,18 +1095,18 @@ if [[ -f "$_env_dir/proxy" ]]; then
|
|
|
1092
1095
|
fi
|
|
1093
1096
|
|
|
1094
1097
|
if [[ -n "$PROXY" ]]; then
|
|
1095
|
-
# pre-flight
|
|
1098
|
+
# pre-flight: proxy connectivity (pure bash, no fork)
|
|
1096
1099
|
_hp="${PROXY##*@}"; _hp="${_hp##*://}"
|
|
1097
1100
|
_host="${_hp%%:*}"
|
|
1098
1101
|
_port="${_hp##*:}"
|
|
1099
1102
|
if ! (echo >/dev/tcp/"$_host"/"$_port") 2>/dev/null; then
|
|
1100
|
-
echo "[cac]
|
|
1101
|
-
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
|
|
1102
1105
|
exit 1
|
|
1103
1106
|
fi
|
|
1104
1107
|
fi
|
|
1105
1108
|
|
|
1106
|
-
#
|
|
1109
|
+
# inject statsig stable_id
|
|
1107
1110
|
if [[ -f "$_env_dir/stable_id" ]]; then
|
|
1108
1111
|
_sid=$(tr -d '[:space:]' < "$_env_dir/stable_id")
|
|
1109
1112
|
_config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
@@ -1116,7 +1119,7 @@ if [[ -f "$_env_dir/stable_id" ]]; then
|
|
|
1116
1119
|
fi
|
|
1117
1120
|
fi
|
|
1118
1121
|
|
|
1119
|
-
#
|
|
1122
|
+
# inject env vars — proxy (only when proxy is configured)
|
|
1120
1123
|
if [[ -n "$PROXY" ]]; then
|
|
1121
1124
|
export _CAC_PROXY="$PROXY"
|
|
1122
1125
|
export HTTPS_PROXY="$PROXY" HTTP_PROXY="$PROXY" ALL_PROXY="$PROXY"
|
|
@@ -1124,45 +1127,45 @@ if [[ -n "$PROXY" ]]; then
|
|
|
1124
1127
|
fi
|
|
1125
1128
|
export PATH="$CAC_DIR/shim-bin:$PATH"
|
|
1126
1129
|
|
|
1127
|
-
# ──
|
|
1128
|
-
# Layer 1: Claude Code
|
|
1130
|
+
# ── multi-layer telemetry protection ──
|
|
1131
|
+
# Layer 1: Claude Code native toggle
|
|
1129
1132
|
export CLAUDE_CODE_ENABLE_TELEMETRY=
|
|
1130
|
-
# Layer 2:
|
|
1133
|
+
# Layer 2: universal telemetry opt-out (https://consoledonottrack.com)
|
|
1131
1134
|
export DO_NOT_TRACK=1
|
|
1132
|
-
# Layer 3: OpenTelemetry SDK
|
|
1135
|
+
# Layer 3: OpenTelemetry SDK fully disabled
|
|
1133
1136
|
export OTEL_SDK_DISABLED=true
|
|
1134
1137
|
export OTEL_TRACES_EXPORTER=none
|
|
1135
1138
|
export OTEL_METRICS_EXPORTER=none
|
|
1136
1139
|
export OTEL_LOGS_EXPORTER=none
|
|
1137
|
-
# Layer 4: Sentry DSN
|
|
1140
|
+
# Layer 4: empty Sentry DSN, block error reporting
|
|
1138
1141
|
export SENTRY_DSN=
|
|
1139
|
-
# Layer 5: Claude Code
|
|
1142
|
+
# Layer 5: Claude Code specific toggles
|
|
1140
1143
|
export DISABLE_ERROR_REPORTING=1
|
|
1141
1144
|
export DISABLE_BUG_COMMAND=1
|
|
1142
1145
|
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
|
1143
|
-
# Layer 6:
|
|
1146
|
+
# Layer 6: other known telemetry flags
|
|
1144
1147
|
export TELEMETRY_DISABLED=1
|
|
1145
1148
|
export DISABLE_TELEMETRY=1
|
|
1146
1149
|
|
|
1147
|
-
#
|
|
1148
|
-
#
|
|
1150
|
+
# with proxy: force OAuth (clear API config to prevent leaks)
|
|
1151
|
+
# without proxy: preserve user's API Key / Base URL
|
|
1149
1152
|
if [[ -n "$PROXY" ]]; then
|
|
1150
1153
|
unset ANTHROPIC_BASE_URL
|
|
1151
1154
|
unset ANTHROPIC_AUTH_TOKEN
|
|
1152
1155
|
unset ANTHROPIC_API_KEY
|
|
1153
1156
|
fi
|
|
1154
1157
|
|
|
1155
|
-
# ── NS
|
|
1158
|
+
# ── NS-level DNS interception ──
|
|
1156
1159
|
if [[ -f "$CAC_DIR/cac-dns-guard.js" ]]; then
|
|
1157
1160
|
case "${NODE_OPTIONS:-}" in
|
|
1158
|
-
*cac-dns-guard.js*) ;; #
|
|
1161
|
+
*cac-dns-guard.js*) ;; # already injected, skip
|
|
1159
1162
|
*) export NODE_OPTIONS="${NODE_OPTIONS:-} --require $CAC_DIR/cac-dns-guard.js" ;;
|
|
1160
1163
|
esac
|
|
1161
1164
|
fi
|
|
1162
|
-
#
|
|
1165
|
+
# fallback layer: HOSTALIASES (gethostbyname level)
|
|
1163
1166
|
[[ -f "$CAC_DIR/blocked_hosts" ]] && export HOSTALIASES="$CAC_DIR/blocked_hosts"
|
|
1164
1167
|
|
|
1165
|
-
# ── mTLS
|
|
1168
|
+
# ── mTLS client certificate ──
|
|
1166
1169
|
if [[ -f "$_env_dir/client_cert.pem" ]] && [[ -f "$_env_dir/client_key.pem" ]]; then
|
|
1167
1170
|
export CAC_MTLS_CERT="$_env_dir/client_cert.pem"
|
|
1168
1171
|
export CAC_MTLS_KEY="$_env_dir/client_key.pem"
|
|
@@ -1173,7 +1176,7 @@ if [[ -f "$_env_dir/client_cert.pem" ]] && [[ -f "$_env_dir/client_key.pem" ]];
|
|
|
1173
1176
|
[[ -n "${_hp:-}" ]] && export CAC_PROXY_HOST="$_hp"
|
|
1174
1177
|
fi
|
|
1175
1178
|
|
|
1176
|
-
#
|
|
1179
|
+
# ensure CA cert is always trusted (required for mTLS)
|
|
1177
1180
|
[[ -f "$CAC_DIR/ca/ca_cert.pem" ]] && export NODE_EXTRA_CA_CERTS="$CAC_DIR/ca/ca_cert.pem"
|
|
1178
1181
|
|
|
1179
1182
|
[[ -f "$_env_dir/tz" ]] && export TZ=$(tr -d '[:space:]' < "$_env_dir/tz")
|
|
@@ -1183,18 +1186,18 @@ if [[ -f "$_env_dir/hostname" ]]; then
|
|
|
1183
1186
|
export HOSTNAME="$_hn" CAC_HOSTNAME="$_hn"
|
|
1184
1187
|
fi
|
|
1185
1188
|
|
|
1186
|
-
# Node.js
|
|
1189
|
+
# Node.js-level fingerprint interception (bypasses shell shim limitations)
|
|
1187
1190
|
[[ -f "$_env_dir/mac_address" ]] && export CAC_MAC=$(tr -d '[:space:]' < "$_env_dir/mac_address")
|
|
1188
1191
|
[[ -f "$_env_dir/machine_id" ]] && export CAC_MACHINE_ID=$(tr -d '[:space:]' < "$_env_dir/machine_id")
|
|
1189
1192
|
export CAC_USERNAME="user-$(echo "$_name" | cut -c1-8)"
|
|
1190
1193
|
if [[ -f "$CAC_DIR/fingerprint-hook.js" ]]; then
|
|
1191
1194
|
case "${NODE_OPTIONS:-}" in
|
|
1192
|
-
*fingerprint-hook.js*) ;; #
|
|
1195
|
+
*fingerprint-hook.js*) ;; # already injected, skip
|
|
1193
1196
|
*) export NODE_OPTIONS="--require $CAC_DIR/fingerprint-hook.js ${NODE_OPTIONS:-}" ;;
|
|
1194
1197
|
esac
|
|
1195
1198
|
fi
|
|
1196
1199
|
|
|
1197
|
-
#
|
|
1200
|
+
# exec real claude — versioned binary or system fallback
|
|
1198
1201
|
_real=""
|
|
1199
1202
|
if [[ -f "$_env_dir/version" ]]; then
|
|
1200
1203
|
_ver=$(tr -d '[:space:]' < "$_env_dir/version")
|
|
@@ -1204,23 +1207,23 @@ fi
|
|
|
1204
1207
|
if [[ -z "$_real" ]] || [[ ! -x "$_real" ]]; then
|
|
1205
1208
|
_real=$(tr -d '[:space:]' < "$CAC_DIR/real_claude")
|
|
1206
1209
|
fi
|
|
1207
|
-
[[ -x "$_real" ]] || { echo "[cac]
|
|
1210
|
+
[[ -x "$_real" ]] || { echo "[cac] error: claude not found, run 'cac setup'" >&2; exit 1; }
|
|
1208
1211
|
|
|
1209
|
-
# ── Relay
|
|
1212
|
+
# ── Relay local forwarding (always enabled when proxy is set) ──
|
|
1210
1213
|
_relay_active=false
|
|
1211
1214
|
if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
1212
1215
|
_relay_js="$CAC_DIR/relay.js"
|
|
1213
1216
|
_relay_pid_file="$CAC_DIR/relay.pid"
|
|
1214
1217
|
_relay_port_file="$CAC_DIR/relay.port"
|
|
1215
1218
|
|
|
1216
|
-
#
|
|
1219
|
+
# check if relay is already running
|
|
1217
1220
|
_relay_running=false
|
|
1218
1221
|
if [[ -f "$_relay_pid_file" ]]; then
|
|
1219
1222
|
_rpid=$(tr -d '[:space:]' < "$_relay_pid_file")
|
|
1220
1223
|
kill -0 "$_rpid" 2>/dev/null && _relay_running=true
|
|
1221
1224
|
fi
|
|
1222
1225
|
|
|
1223
|
-
#
|
|
1226
|
+
# start if not running
|
|
1224
1227
|
if [[ "$_relay_running" != "true" ]] && [[ -f "$_relay_js" ]]; then
|
|
1225
1228
|
_rport=17890
|
|
1226
1229
|
while (echo >/dev/tcp/127.0.0.1/$_rport) 2>/dev/null; do
|
|
@@ -1236,7 +1239,7 @@ if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
|
1236
1239
|
echo "$_rport" > "$_relay_port_file"
|
|
1237
1240
|
fi
|
|
1238
1241
|
|
|
1239
|
-
#
|
|
1242
|
+
# override proxy to point to local relay
|
|
1240
1243
|
if [[ -f "$_relay_port_file" ]]; then
|
|
1241
1244
|
_rport=$(tr -d '[:space:]' < "$_relay_port_file")
|
|
1242
1245
|
export HTTPS_PROXY="http://127.0.0.1:$_rport"
|
|
@@ -1246,9 +1249,9 @@ if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
|
1246
1249
|
fi
|
|
1247
1250
|
fi
|
|
1248
1251
|
|
|
1249
|
-
#
|
|
1252
|
+
# cleanup function
|
|
1250
1253
|
_cleanup_all() {
|
|
1251
|
-
#
|
|
1254
|
+
# cleanup relay
|
|
1252
1255
|
if [[ "$_relay_active" == "true" ]] && [[ -f "$CAC_DIR/relay.pid" ]]; then
|
|
1253
1256
|
local _p; _p=$(cat "$CAC_DIR/relay.pid" 2>/dev/null) || true
|
|
1254
1257
|
[[ -n "$_p" ]] && kill "$_p" 2>/dev/null || true
|
|
@@ -1271,7 +1274,7 @@ _write_ioreg_shim() {
|
|
|
1271
1274
|
#!/usr/bin/env bash
|
|
1272
1275
|
CAC_DIR="$HOME/.cac"
|
|
1273
1276
|
|
|
1274
|
-
#
|
|
1277
|
+
# non-target call: passthrough to real ioreg
|
|
1275
1278
|
if ! echo "$*" | grep -q "IOPlatformExpertDevice"; then
|
|
1276
1279
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
|
|
1277
1280
|
command -v ioreg 2>/dev/null || true)
|
|
@@ -1279,7 +1282,7 @@ if ! echo "$*" | grep -q "IOPlatformExpertDevice"; then
|
|
|
1279
1282
|
exit 0
|
|
1280
1283
|
fi
|
|
1281
1284
|
|
|
1282
|
-
#
|
|
1285
|
+
# read current env UUID
|
|
1283
1286
|
_uuid_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/uuid"
|
|
1284
1287
|
if [[ ! -f "$_uuid_file" ]]; then
|
|
1285
1288
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
|
|
@@ -1309,10 +1312,10 @@ _write_machine_id_shim() {
|
|
|
1309
1312
|
#!/usr/bin/env bash
|
|
1310
1313
|
CAC_DIR="$HOME/.cac"
|
|
1311
1314
|
|
|
1312
|
-
#
|
|
1315
|
+
# get real cat path first (avoid recursive self-call)
|
|
1313
1316
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v cat 2>/dev/null || true)
|
|
1314
1317
|
|
|
1315
|
-
#
|
|
1318
|
+
# intercept /etc/machine-id and /var/lib/dbus/machine-id
|
|
1316
1319
|
if [[ "$1" == "/etc/machine-id" ]] || [[ "$1" == "/var/lib/dbus/machine-id" ]]; then
|
|
1317
1320
|
_mid_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/machine_id"
|
|
1318
1321
|
if [[ -f "$_mid_file" ]] && [[ -n "$_real" ]]; then
|
|
@@ -1320,7 +1323,7 @@ if [[ "$1" == "/etc/machine-id" ]] || [[ "$1" == "/var/lib/dbus/machine-id" ]];
|
|
|
1320
1323
|
fi
|
|
1321
1324
|
fi
|
|
1322
1325
|
|
|
1323
|
-
#
|
|
1326
|
+
# non-target call: passthrough to real cat
|
|
1324
1327
|
[[ -n "$_real" ]] && exec "$_real" "$@"
|
|
1325
1328
|
exit 1
|
|
1326
1329
|
CAT_EOF
|
|
@@ -1333,14 +1336,14 @@ _write_hostname_shim() {
|
|
|
1333
1336
|
#!/usr/bin/env bash
|
|
1334
1337
|
CAC_DIR="$HOME/.cac"
|
|
1335
1338
|
|
|
1336
|
-
#
|
|
1339
|
+
# read spoofed hostname
|
|
1337
1340
|
_hn_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/hostname"
|
|
1338
1341
|
if [[ -f "$_hn_file" ]]; then
|
|
1339
1342
|
tr -d '[:space:]' < "$_hn_file"
|
|
1340
1343
|
exit 0
|
|
1341
1344
|
fi
|
|
1342
1345
|
|
|
1343
|
-
#
|
|
1346
|
+
# passthrough to real hostname
|
|
1344
1347
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v hostname 2>/dev/null || true)
|
|
1345
1348
|
[[ -n "$_real" ]] && exec "$_real" "$@"
|
|
1346
1349
|
exit 1
|
|
@@ -1785,7 +1788,7 @@ cmd_env() {
|
|
|
1785
1788
|
}
|
|
1786
1789
|
|
|
1787
1790
|
# ━━━ cmd_relay.sh ━━━
|
|
1788
|
-
# ── cmd: relay
|
|
1791
|
+
# ── cmd: relay (local relay, bypass TUN) ──────────────────────────────
|
|
1789
1792
|
|
|
1790
1793
|
_relay_start() {
|
|
1791
1794
|
local name="${1:-$(_current_env)}"
|
|
@@ -1794,14 +1797,14 @@ _relay_start() {
|
|
|
1794
1797
|
[[ -z "$proxy" ]] && return 1
|
|
1795
1798
|
|
|
1796
1799
|
local relay_js="$CAC_DIR/relay.js"
|
|
1797
|
-
[[ -f "$relay_js" ]] || { echo "
|
|
1800
|
+
[[ -f "$relay_js" ]] || { echo "error: relay.js not found, run 'cac setup'" >&2; return 1; }
|
|
1798
1801
|
|
|
1799
|
-
#
|
|
1802
|
+
# find available port (17890-17999)
|
|
1800
1803
|
local port=17890
|
|
1801
1804
|
while (echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null; do
|
|
1802
1805
|
(( port++ ))
|
|
1803
1806
|
if [[ $port -gt 17999 ]]; then
|
|
1804
|
-
echo "
|
|
1807
|
+
echo "error: all ports 17890-17999 occupied" >&2
|
|
1805
1808
|
return 1
|
|
1806
1809
|
fi
|
|
1807
1810
|
done
|
|
@@ -1810,7 +1813,7 @@ _relay_start() {
|
|
|
1810
1813
|
node "$relay_js" "$port" "$proxy" "$pid_file" </dev/null >"$CAC_DIR/relay.log" 2>&1 &
|
|
1811
1814
|
disown
|
|
1812
1815
|
|
|
1813
|
-
#
|
|
1816
|
+
# wait for relay ready
|
|
1814
1817
|
local _i
|
|
1815
1818
|
for _i in {1..30}; do
|
|
1816
1819
|
(echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null && break
|
|
@@ -1818,7 +1821,7 @@ _relay_start() {
|
|
|
1818
1821
|
done
|
|
1819
1822
|
|
|
1820
1823
|
if ! (echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null; then
|
|
1821
|
-
echo "
|
|
1824
|
+
echo "error: relay startup timeout" >&2
|
|
1822
1825
|
return 1
|
|
1823
1826
|
fi
|
|
1824
1827
|
|
|
@@ -1832,7 +1835,7 @@ _relay_stop() {
|
|
|
1832
1835
|
local pid; pid=$(tr -d '[:space:]' < "$pid_file")
|
|
1833
1836
|
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
1834
1837
|
kill "$pid" 2>/dev/null
|
|
1835
|
-
#
|
|
1838
|
+
# wait for process exit
|
|
1836
1839
|
local _i
|
|
1837
1840
|
for _i in {1..20}; do
|
|
1838
1841
|
kill -0 "$pid" 2>/dev/null || break
|
|
@@ -1843,7 +1846,7 @@ _relay_stop() {
|
|
|
1843
1846
|
fi
|
|
1844
1847
|
rm -f "$CAC_DIR/relay.port"
|
|
1845
1848
|
|
|
1846
|
-
#
|
|
1849
|
+
# cleanup route
|
|
1847
1850
|
_relay_remove_route 2>/dev/null || true
|
|
1848
1851
|
}
|
|
1849
1852
|
|
|
@@ -1854,17 +1857,17 @@ _relay_is_running() {
|
|
|
1854
1857
|
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
|
|
1855
1858
|
}
|
|
1856
1859
|
|
|
1857
|
-
# ──
|
|
1860
|
+
# ── route management (direct route to bypass TUN) ──────────────────────────────
|
|
1858
1861
|
|
|
1859
1862
|
_relay_add_route() {
|
|
1860
1863
|
local proxy="$1"
|
|
1861
1864
|
local proxy_host; proxy_host=$(_proxy_host_port "$proxy")
|
|
1862
1865
|
proxy_host="${proxy_host%%:*}"
|
|
1863
1866
|
|
|
1864
|
-
#
|
|
1867
|
+
# skip loopback addresses
|
|
1865
1868
|
[[ "$proxy_host" == "127."* || "$proxy_host" == "localhost" ]] && return 0
|
|
1866
1869
|
|
|
1867
|
-
#
|
|
1870
|
+
# resolve to IP
|
|
1868
1871
|
local proxy_ip
|
|
1869
1872
|
proxy_ip=$(python3 -c "import socket; print(socket.gethostbyname('$proxy_host'))" 2>/dev/null || echo "$proxy_host")
|
|
1870
1873
|
|
|
@@ -1874,12 +1877,12 @@ _relay_add_route() {
|
|
|
1874
1877
|
gateway=$(route -n get default 2>/dev/null | awk '/gateway:/{print $2}')
|
|
1875
1878
|
[[ -z "$gateway" ]] && return 1
|
|
1876
1879
|
|
|
1877
|
-
#
|
|
1880
|
+
# check if direct route exists
|
|
1878
1881
|
local current_gw
|
|
1879
1882
|
current_gw=$(route -n get "$proxy_ip" 2>/dev/null | awk '/gateway:/{print $2}')
|
|
1880
1883
|
[[ "$current_gw" == "$gateway" ]] && return 0
|
|
1881
1884
|
|
|
1882
|
-
echo "
|
|
1885
|
+
echo " adding direct route: $proxy_ip -> $gateway (needs sudo)"
|
|
1883
1886
|
sudo route add -host "$proxy_ip" "$gateway" >/dev/null 2>&1 || return 1
|
|
1884
1887
|
echo "$proxy_ip" > "$CAC_DIR/relay_route_ip"
|
|
1885
1888
|
|
|
@@ -1889,7 +1892,7 @@ _relay_add_route() {
|
|
|
1889
1892
|
iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
|
1890
1893
|
[[ -z "$gateway" ]] && return 1
|
|
1891
1894
|
|
|
1892
|
-
echo "
|
|
1895
|
+
echo " adding direct route: $proxy_ip -> $gateway dev $iface (needs sudo)"
|
|
1893
1896
|
sudo ip route add "$proxy_ip/32" via "$gateway" dev "$iface" 2>/dev/null || return 1
|
|
1894
1897
|
echo "$proxy_ip" > "$CAC_DIR/relay_route_ip"
|
|
1895
1898
|
fi
|
|
@@ -1911,7 +1914,7 @@ _relay_remove_route() {
|
|
|
1911
1914
|
rm -f "$route_file"
|
|
1912
1915
|
}
|
|
1913
1916
|
|
|
1914
|
-
#
|
|
1917
|
+
# detect if TUN interface is active
|
|
1915
1918
|
_detect_tun_active() {
|
|
1916
1919
|
local os; os=$(_detect_os)
|
|
1917
1920
|
if [[ "$os" == "macos" ]]; then
|
|
@@ -1925,12 +1928,12 @@ _detect_tun_active() {
|
|
|
1925
1928
|
fi
|
|
1926
1929
|
}
|
|
1927
1930
|
|
|
1928
|
-
# ──
|
|
1931
|
+
# ── user commands ─────────────────────────────────────────────────────
|
|
1929
1932
|
|
|
1930
1933
|
cmd_relay() {
|
|
1931
1934
|
_require_setup
|
|
1932
1935
|
local current; current=$(_current_env)
|
|
1933
|
-
[[ -z "$current" ]] && { echo "
|
|
1936
|
+
[[ -z "$current" ]] && { echo "error: no active environment, run 'cac <name>' first" >&2; exit 1; }
|
|
1934
1937
|
|
|
1935
1938
|
local env_dir="$ENVS_DIR/$current"
|
|
1936
1939
|
local action="${1:-status}"
|
|
@@ -1939,60 +1942,60 @@ cmd_relay() {
|
|
|
1939
1942
|
case "$action" in
|
|
1940
1943
|
on)
|
|
1941
1944
|
echo "on" > "$env_dir/relay"
|
|
1942
|
-
echo "$(_green "✓") Relay
|
|
1945
|
+
echo "$(_green "✓") Relay enabled (env: $(_bold "$current"))"
|
|
1943
1946
|
|
|
1944
|
-
# --route
|
|
1947
|
+
# --route flag: add direct route
|
|
1945
1948
|
if [[ "$flag" == "--route" ]]; then
|
|
1946
1949
|
local proxy; proxy=$(_read "$env_dir/proxy")
|
|
1947
1950
|
_relay_add_route "$proxy"
|
|
1948
1951
|
fi
|
|
1949
1952
|
|
|
1950
|
-
#
|
|
1953
|
+
# start relay if not running
|
|
1951
1954
|
if ! _relay_is_running; then
|
|
1952
|
-
printf "
|
|
1955
|
+
printf " starting relay ... "
|
|
1953
1956
|
if _relay_start "$current"; then
|
|
1954
1957
|
local port; port=$(_read "$CAC_DIR/relay.port")
|
|
1955
1958
|
echo "$(_green "✓") 127.0.0.1:$port"
|
|
1956
1959
|
else
|
|
1957
|
-
echo "$(_red "✗
|
|
1960
|
+
echo "$(_red "✗ failed to start")"
|
|
1958
1961
|
fi
|
|
1959
1962
|
fi
|
|
1960
|
-
echo "
|
|
1963
|
+
echo " next claude launch will automatically connect via local relay"
|
|
1961
1964
|
;;
|
|
1962
1965
|
off)
|
|
1963
1966
|
rm -f "$env_dir/relay"
|
|
1964
1967
|
_relay_stop
|
|
1965
|
-
echo "$(_green "✓") Relay
|
|
1968
|
+
echo "$(_green "✓") Relay disabled (env: $(_bold "$current"))"
|
|
1966
1969
|
;;
|
|
1967
1970
|
status)
|
|
1968
1971
|
if [[ -f "$env_dir/relay" ]] && [[ "$(_read "$env_dir/relay")" == "on" ]]; then
|
|
1969
|
-
echo "Relay
|
|
1972
|
+
echo "Relay mode: $(_green "enabled")"
|
|
1970
1973
|
else
|
|
1971
|
-
echo "Relay
|
|
1974
|
+
echo "Relay mode: disabled"
|
|
1972
1975
|
if _detect_tun_active; then
|
|
1973
|
-
echo " $(_yellow "⚠")
|
|
1976
|
+
echo " $(_yellow "⚠") TUN mode detected, consider running 'cac relay on'"
|
|
1974
1977
|
fi
|
|
1975
1978
|
return
|
|
1976
1979
|
fi
|
|
1977
1980
|
|
|
1978
1981
|
if _relay_is_running; then
|
|
1979
1982
|
local pid; pid=$(_read "$CAC_DIR/relay.pid")
|
|
1980
|
-
local port; port=$(_read "$CAC_DIR/relay.port" "
|
|
1981
|
-
echo "Relay
|
|
1983
|
+
local port; port=$(_read "$CAC_DIR/relay.port" "unknown")
|
|
1984
|
+
echo "Relay process: $(_green "running") (PID=$pid, port=$port)"
|
|
1982
1985
|
else
|
|
1983
|
-
echo "Relay
|
|
1986
|
+
echo "Relay process: $(_yellow "not started") (will auto-start with claude)"
|
|
1984
1987
|
fi
|
|
1985
1988
|
|
|
1986
1989
|
if [[ -f "$CAC_DIR/relay_route_ip" ]]; then
|
|
1987
1990
|
local route_ip; route_ip=$(_read "$CAC_DIR/relay_route_ip")
|
|
1988
|
-
echo "
|
|
1991
|
+
echo "Direct route: $route_ip"
|
|
1989
1992
|
fi
|
|
1990
1993
|
;;
|
|
1991
1994
|
*)
|
|
1992
|
-
echo "
|
|
1993
|
-
echo " on [--route]
|
|
1994
|
-
echo " off
|
|
1995
|
-
echo " status
|
|
1995
|
+
echo "usage: cac relay [on|off|status]" >&2
|
|
1996
|
+
echo " on [--route] enable local relay (--route adds direct route to bypass TUN)" >&2
|
|
1997
|
+
echo " off disable local relay" >&2
|
|
1998
|
+
echo " status show status" >&2
|
|
1996
1999
|
;;
|
|
1997
2000
|
esac
|
|
1998
2001
|
}
|
|
@@ -2196,17 +2199,17 @@ cmd_stop() {
|
|
|
2196
2199
|
|
|
2197
2200
|
cmd_continue() {
|
|
2198
2201
|
if [[ ! -f "$CAC_DIR/stopped" ]]; then
|
|
2199
|
-
echo "cac
|
|
2202
|
+
echo "cac is not stopped, no need to resume"
|
|
2200
2203
|
return
|
|
2201
2204
|
fi
|
|
2202
2205
|
|
|
2203
2206
|
local current; current=$(_current_env)
|
|
2204
2207
|
if [[ -z "$current" ]]; then
|
|
2205
|
-
echo "
|
|
2208
|
+
echo "error: no active environment, run 'cac <name>'" >&2; exit 1
|
|
2206
2209
|
fi
|
|
2207
2210
|
|
|
2208
2211
|
rm -f "$CAC_DIR/stopped"
|
|
2209
|
-
echo "$(_green "✓") cac
|
|
2212
|
+
echo "$(_green "✓") cac resumed — current env: $(_bold "$current")"
|
|
2210
2213
|
}
|
|
2211
2214
|
|
|
2212
2215
|
# ━━━ cmd_claude.sh ━━━
|
|
@@ -2936,7 +2939,7 @@ cmd_docker() {
|
|
|
2936
2939
|
}
|
|
2937
2940
|
|
|
2938
2941
|
# ━━━ cmd_delete.sh ━━━
|
|
2939
|
-
# ── cmd: delete
|
|
2942
|
+
# ── cmd: delete (uninstall) ────────────────────────────────────────
|
|
2940
2943
|
|
|
2941
2944
|
cmd_delete() {
|
|
2942
2945
|
echo "=== cac delete ==="
|
|
@@ -2947,50 +2950,50 @@ cmd_delete() {
|
|
|
2947
2950
|
|
|
2948
2951
|
_remove_path_from_rc "$rc_file"
|
|
2949
2952
|
|
|
2950
|
-
#
|
|
2953
|
+
# stop relay processes and routes
|
|
2951
2954
|
if [[ -d "$CAC_DIR" ]]; then
|
|
2952
2955
|
_relay_stop 2>/dev/null || true
|
|
2953
2956
|
|
|
2954
|
-
#
|
|
2957
|
+
# stop docker port-forward processes
|
|
2955
2958
|
if [[ -d /tmp/cac-docker-ports ]]; then
|
|
2956
2959
|
for _pf in /tmp/cac-docker-ports/*.pid; do
|
|
2957
2960
|
[[ -f "$_pf" ]] || continue
|
|
2958
2961
|
kill "$(cat "$_pf")" 2>/dev/null || true
|
|
2959
2962
|
rm -f "$_pf"
|
|
2960
2963
|
done
|
|
2961
|
-
echo " ✓
|
|
2964
|
+
echo " ✓ stopped docker port-forward processes"
|
|
2962
2965
|
fi
|
|
2963
2966
|
|
|
2964
|
-
#
|
|
2967
|
+
# fallback: clean up orphaned relay processes
|
|
2965
2968
|
pkill -f "node.*\.cac/relay\.js" 2>/dev/null || true
|
|
2966
2969
|
|
|
2967
2970
|
rm -rf "$CAC_DIR"
|
|
2968
|
-
echo " ✓
|
|
2971
|
+
echo " ✓ deleted $CAC_DIR"
|
|
2969
2972
|
else
|
|
2970
|
-
echo " - $CAC_DIR
|
|
2973
|
+
echo " - $CAC_DIR does not exist, skipping"
|
|
2971
2974
|
fi
|
|
2972
2975
|
|
|
2973
2976
|
local method
|
|
2974
2977
|
method=$(_install_method)
|
|
2975
2978
|
echo
|
|
2976
2979
|
if [[ "$method" == "npm" ]]; then
|
|
2977
|
-
echo " ✓
|
|
2980
|
+
echo " ✓ cleared all cac data and config"
|
|
2978
2981
|
echo
|
|
2979
|
-
echo "
|
|
2982
|
+
echo "to fully uninstall the cac command, run:"
|
|
2980
2983
|
echo " npm uninstall -g claude-cac"
|
|
2981
2984
|
else
|
|
2982
2985
|
if [[ -f "$HOME/bin/cac" ]]; then
|
|
2983
2986
|
rm -f "$HOME/bin/cac"
|
|
2984
|
-
echo " ✓
|
|
2987
|
+
echo " ✓ deleted $HOME/bin/cac"
|
|
2985
2988
|
fi
|
|
2986
|
-
echo " ✓
|
|
2989
|
+
echo " ✓ uninstall complete"
|
|
2987
2990
|
fi
|
|
2988
2991
|
|
|
2989
2992
|
echo
|
|
2990
2993
|
if [[ -n "$rc_file" ]]; then
|
|
2991
|
-
echo "
|
|
2994
|
+
echo "please restart terminal or run source $rc_file for changes to take effect."
|
|
2992
2995
|
else
|
|
2993
|
-
echo "
|
|
2996
|
+
echo "please restart terminal for changes to take effect."
|
|
2994
2997
|
fi
|
|
2995
2998
|
}
|
|
2996
2999
|
|
|
@@ -3042,7 +3045,7 @@ cmd_help() {
|
|
|
3042
3045
|
}
|
|
3043
3046
|
|
|
3044
3047
|
# ━━━ main.sh ━━━
|
|
3045
|
-
# ──
|
|
3048
|
+
# ── entry: dispatch commands ──────────────────────────────────────────────
|
|
3046
3049
|
|
|
3047
3050
|
[[ $# -eq 0 ]] && { cmd_help; exit 0; }
|
|
3048
3051
|
|