claude-cac 1.3.1 → 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 +184 -184
- 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
|
|
@@ -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,45 @@ 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
1164
|
fi
|
|
1165
|
-
#
|
|
1165
|
+
# fallback layer: HOSTALIASES (gethostbyname level)
|
|
1166
1166
|
[[ -f "$CAC_DIR/blocked_hosts" ]] && export HOSTALIASES="$CAC_DIR/blocked_hosts"
|
|
1167
1167
|
|
|
1168
|
-
# ── mTLS
|
|
1168
|
+
# ── mTLS client certificate ──
|
|
1169
1169
|
if [[ -f "$_env_dir/client_cert.pem" ]] && [[ -f "$_env_dir/client_key.pem" ]]; then
|
|
1170
1170
|
export CAC_MTLS_CERT="$_env_dir/client_cert.pem"
|
|
1171
1171
|
export CAC_MTLS_KEY="$_env_dir/client_key.pem"
|
|
@@ -1176,7 +1176,7 @@ if [[ -f "$_env_dir/client_cert.pem" ]] && [[ -f "$_env_dir/client_key.pem" ]];
|
|
|
1176
1176
|
[[ -n "${_hp:-}" ]] && export CAC_PROXY_HOST="$_hp"
|
|
1177
1177
|
fi
|
|
1178
1178
|
|
|
1179
|
-
#
|
|
1179
|
+
# ensure CA cert is always trusted (required for mTLS)
|
|
1180
1180
|
[[ -f "$CAC_DIR/ca/ca_cert.pem" ]] && export NODE_EXTRA_CA_CERTS="$CAC_DIR/ca/ca_cert.pem"
|
|
1181
1181
|
|
|
1182
1182
|
[[ -f "$_env_dir/tz" ]] && export TZ=$(tr -d '[:space:]' < "$_env_dir/tz")
|
|
@@ -1186,18 +1186,18 @@ if [[ -f "$_env_dir/hostname" ]]; then
|
|
|
1186
1186
|
export HOSTNAME="$_hn" CAC_HOSTNAME="$_hn"
|
|
1187
1187
|
fi
|
|
1188
1188
|
|
|
1189
|
-
# Node.js
|
|
1189
|
+
# Node.js-level fingerprint interception (bypasses shell shim limitations)
|
|
1190
1190
|
[[ -f "$_env_dir/mac_address" ]] && export CAC_MAC=$(tr -d '[:space:]' < "$_env_dir/mac_address")
|
|
1191
1191
|
[[ -f "$_env_dir/machine_id" ]] && export CAC_MACHINE_ID=$(tr -d '[:space:]' < "$_env_dir/machine_id")
|
|
1192
1192
|
export CAC_USERNAME="user-$(echo "$_name" | cut -c1-8)"
|
|
1193
1193
|
if [[ -f "$CAC_DIR/fingerprint-hook.js" ]]; then
|
|
1194
1194
|
case "${NODE_OPTIONS:-}" in
|
|
1195
|
-
*fingerprint-hook.js*) ;; #
|
|
1195
|
+
*fingerprint-hook.js*) ;; # already injected, skip
|
|
1196
1196
|
*) export NODE_OPTIONS="--require $CAC_DIR/fingerprint-hook.js ${NODE_OPTIONS:-}" ;;
|
|
1197
1197
|
esac
|
|
1198
1198
|
fi
|
|
1199
1199
|
|
|
1200
|
-
#
|
|
1200
|
+
# exec real claude — versioned binary or system fallback
|
|
1201
1201
|
_real=""
|
|
1202
1202
|
if [[ -f "$_env_dir/version" ]]; then
|
|
1203
1203
|
_ver=$(tr -d '[:space:]' < "$_env_dir/version")
|
|
@@ -1207,23 +1207,23 @@ fi
|
|
|
1207
1207
|
if [[ -z "$_real" ]] || [[ ! -x "$_real" ]]; then
|
|
1208
1208
|
_real=$(tr -d '[:space:]' < "$CAC_DIR/real_claude")
|
|
1209
1209
|
fi
|
|
1210
|
-
[[ -x "$_real" ]] || { echo "[cac]
|
|
1210
|
+
[[ -x "$_real" ]] || { echo "[cac] error: claude not found, run 'cac setup'" >&2; exit 1; }
|
|
1211
1211
|
|
|
1212
|
-
# ── Relay
|
|
1212
|
+
# ── Relay local forwarding (always enabled when proxy is set) ──
|
|
1213
1213
|
_relay_active=false
|
|
1214
1214
|
if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
1215
1215
|
_relay_js="$CAC_DIR/relay.js"
|
|
1216
1216
|
_relay_pid_file="$CAC_DIR/relay.pid"
|
|
1217
1217
|
_relay_port_file="$CAC_DIR/relay.port"
|
|
1218
1218
|
|
|
1219
|
-
#
|
|
1219
|
+
# check if relay is already running
|
|
1220
1220
|
_relay_running=false
|
|
1221
1221
|
if [[ -f "$_relay_pid_file" ]]; then
|
|
1222
1222
|
_rpid=$(tr -d '[:space:]' < "$_relay_pid_file")
|
|
1223
1223
|
kill -0 "$_rpid" 2>/dev/null && _relay_running=true
|
|
1224
1224
|
fi
|
|
1225
1225
|
|
|
1226
|
-
#
|
|
1226
|
+
# start if not running
|
|
1227
1227
|
if [[ "$_relay_running" != "true" ]] && [[ -f "$_relay_js" ]]; then
|
|
1228
1228
|
_rport=17890
|
|
1229
1229
|
while (echo >/dev/tcp/127.0.0.1/$_rport) 2>/dev/null; do
|
|
@@ -1239,7 +1239,7 @@ if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
|
1239
1239
|
echo "$_rport" > "$_relay_port_file"
|
|
1240
1240
|
fi
|
|
1241
1241
|
|
|
1242
|
-
#
|
|
1242
|
+
# override proxy to point to local relay
|
|
1243
1243
|
if [[ -f "$_relay_port_file" ]]; then
|
|
1244
1244
|
_rport=$(tr -d '[:space:]' < "$_relay_port_file")
|
|
1245
1245
|
export HTTPS_PROXY="http://127.0.0.1:$_rport"
|
|
@@ -1249,9 +1249,9 @@ if [[ -n "$PROXY" ]] && [[ -f "$CAC_DIR/relay.js" ]]; then
|
|
|
1249
1249
|
fi
|
|
1250
1250
|
fi
|
|
1251
1251
|
|
|
1252
|
-
#
|
|
1252
|
+
# cleanup function
|
|
1253
1253
|
_cleanup_all() {
|
|
1254
|
-
#
|
|
1254
|
+
# cleanup relay
|
|
1255
1255
|
if [[ "$_relay_active" == "true" ]] && [[ -f "$CAC_DIR/relay.pid" ]]; then
|
|
1256
1256
|
local _p; _p=$(cat "$CAC_DIR/relay.pid" 2>/dev/null) || true
|
|
1257
1257
|
[[ -n "$_p" ]] && kill "$_p" 2>/dev/null || true
|
|
@@ -1274,7 +1274,7 @@ _write_ioreg_shim() {
|
|
|
1274
1274
|
#!/usr/bin/env bash
|
|
1275
1275
|
CAC_DIR="$HOME/.cac"
|
|
1276
1276
|
|
|
1277
|
-
#
|
|
1277
|
+
# non-target call: passthrough to real ioreg
|
|
1278
1278
|
if ! echo "$*" | grep -q "IOPlatformExpertDevice"; then
|
|
1279
1279
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
|
|
1280
1280
|
command -v ioreg 2>/dev/null || true)
|
|
@@ -1282,7 +1282,7 @@ if ! echo "$*" | grep -q "IOPlatformExpertDevice"; then
|
|
|
1282
1282
|
exit 0
|
|
1283
1283
|
fi
|
|
1284
1284
|
|
|
1285
|
-
#
|
|
1285
|
+
# read current env UUID
|
|
1286
1286
|
_uuid_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/uuid"
|
|
1287
1287
|
if [[ ! -f "$_uuid_file" ]]; then
|
|
1288
1288
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
|
|
@@ -1312,10 +1312,10 @@ _write_machine_id_shim() {
|
|
|
1312
1312
|
#!/usr/bin/env bash
|
|
1313
1313
|
CAC_DIR="$HOME/.cac"
|
|
1314
1314
|
|
|
1315
|
-
#
|
|
1315
|
+
# get real cat path first (avoid recursive self-call)
|
|
1316
1316
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v cat 2>/dev/null || true)
|
|
1317
1317
|
|
|
1318
|
-
#
|
|
1318
|
+
# intercept /etc/machine-id and /var/lib/dbus/machine-id
|
|
1319
1319
|
if [[ "$1" == "/etc/machine-id" ]] || [[ "$1" == "/var/lib/dbus/machine-id" ]]; then
|
|
1320
1320
|
_mid_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/machine_id"
|
|
1321
1321
|
if [[ -f "$_mid_file" ]] && [[ -n "$_real" ]]; then
|
|
@@ -1323,7 +1323,7 @@ if [[ "$1" == "/etc/machine-id" ]] || [[ "$1" == "/var/lib/dbus/machine-id" ]];
|
|
|
1323
1323
|
fi
|
|
1324
1324
|
fi
|
|
1325
1325
|
|
|
1326
|
-
#
|
|
1326
|
+
# non-target call: passthrough to real cat
|
|
1327
1327
|
[[ -n "$_real" ]] && exec "$_real" "$@"
|
|
1328
1328
|
exit 1
|
|
1329
1329
|
CAT_EOF
|
|
@@ -1336,14 +1336,14 @@ _write_hostname_shim() {
|
|
|
1336
1336
|
#!/usr/bin/env bash
|
|
1337
1337
|
CAC_DIR="$HOME/.cac"
|
|
1338
1338
|
|
|
1339
|
-
#
|
|
1339
|
+
# read spoofed hostname
|
|
1340
1340
|
_hn_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/hostname"
|
|
1341
1341
|
if [[ -f "$_hn_file" ]]; then
|
|
1342
1342
|
tr -d '[:space:]' < "$_hn_file"
|
|
1343
1343
|
exit 0
|
|
1344
1344
|
fi
|
|
1345
1345
|
|
|
1346
|
-
#
|
|
1346
|
+
# passthrough to real hostname
|
|
1347
1347
|
_real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v hostname 2>/dev/null || true)
|
|
1348
1348
|
[[ -n "$_real" ]] && exec "$_real" "$@"
|
|
1349
1349
|
exit 1
|
|
@@ -1788,7 +1788,7 @@ cmd_env() {
|
|
|
1788
1788
|
}
|
|
1789
1789
|
|
|
1790
1790
|
# ━━━ cmd_relay.sh ━━━
|
|
1791
|
-
# ── cmd: relay
|
|
1791
|
+
# ── cmd: relay (local relay, bypass TUN) ──────────────────────────────
|
|
1792
1792
|
|
|
1793
1793
|
_relay_start() {
|
|
1794
1794
|
local name="${1:-$(_current_env)}"
|
|
@@ -1797,14 +1797,14 @@ _relay_start() {
|
|
|
1797
1797
|
[[ -z "$proxy" ]] && return 1
|
|
1798
1798
|
|
|
1799
1799
|
local relay_js="$CAC_DIR/relay.js"
|
|
1800
|
-
[[ -f "$relay_js" ]] || { echo "
|
|
1800
|
+
[[ -f "$relay_js" ]] || { echo "error: relay.js not found, run 'cac setup'" >&2; return 1; }
|
|
1801
1801
|
|
|
1802
|
-
#
|
|
1802
|
+
# find available port (17890-17999)
|
|
1803
1803
|
local port=17890
|
|
1804
1804
|
while (echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null; do
|
|
1805
1805
|
(( port++ ))
|
|
1806
1806
|
if [[ $port -gt 17999 ]]; then
|
|
1807
|
-
echo "
|
|
1807
|
+
echo "error: all ports 17890-17999 occupied" >&2
|
|
1808
1808
|
return 1
|
|
1809
1809
|
fi
|
|
1810
1810
|
done
|
|
@@ -1813,7 +1813,7 @@ _relay_start() {
|
|
|
1813
1813
|
node "$relay_js" "$port" "$proxy" "$pid_file" </dev/null >"$CAC_DIR/relay.log" 2>&1 &
|
|
1814
1814
|
disown
|
|
1815
1815
|
|
|
1816
|
-
#
|
|
1816
|
+
# wait for relay ready
|
|
1817
1817
|
local _i
|
|
1818
1818
|
for _i in {1..30}; do
|
|
1819
1819
|
(echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null && break
|
|
@@ -1821,7 +1821,7 @@ _relay_start() {
|
|
|
1821
1821
|
done
|
|
1822
1822
|
|
|
1823
1823
|
if ! (echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null; then
|
|
1824
|
-
echo "
|
|
1824
|
+
echo "error: relay startup timeout" >&2
|
|
1825
1825
|
return 1
|
|
1826
1826
|
fi
|
|
1827
1827
|
|
|
@@ -1835,7 +1835,7 @@ _relay_stop() {
|
|
|
1835
1835
|
local pid; pid=$(tr -d '[:space:]' < "$pid_file")
|
|
1836
1836
|
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
1837
1837
|
kill "$pid" 2>/dev/null
|
|
1838
|
-
#
|
|
1838
|
+
# wait for process exit
|
|
1839
1839
|
local _i
|
|
1840
1840
|
for _i in {1..20}; do
|
|
1841
1841
|
kill -0 "$pid" 2>/dev/null || break
|
|
@@ -1846,7 +1846,7 @@ _relay_stop() {
|
|
|
1846
1846
|
fi
|
|
1847
1847
|
rm -f "$CAC_DIR/relay.port"
|
|
1848
1848
|
|
|
1849
|
-
#
|
|
1849
|
+
# cleanup route
|
|
1850
1850
|
_relay_remove_route 2>/dev/null || true
|
|
1851
1851
|
}
|
|
1852
1852
|
|
|
@@ -1857,17 +1857,17 @@ _relay_is_running() {
|
|
|
1857
1857
|
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
|
|
1858
1858
|
}
|
|
1859
1859
|
|
|
1860
|
-
# ──
|
|
1860
|
+
# ── route management (direct route to bypass TUN) ──────────────────────────────
|
|
1861
1861
|
|
|
1862
1862
|
_relay_add_route() {
|
|
1863
1863
|
local proxy="$1"
|
|
1864
1864
|
local proxy_host; proxy_host=$(_proxy_host_port "$proxy")
|
|
1865
1865
|
proxy_host="${proxy_host%%:*}"
|
|
1866
1866
|
|
|
1867
|
-
#
|
|
1867
|
+
# skip loopback addresses
|
|
1868
1868
|
[[ "$proxy_host" == "127."* || "$proxy_host" == "localhost" ]] && return 0
|
|
1869
1869
|
|
|
1870
|
-
#
|
|
1870
|
+
# resolve to IP
|
|
1871
1871
|
local proxy_ip
|
|
1872
1872
|
proxy_ip=$(python3 -c "import socket; print(socket.gethostbyname('$proxy_host'))" 2>/dev/null || echo "$proxy_host")
|
|
1873
1873
|
|
|
@@ -1877,12 +1877,12 @@ _relay_add_route() {
|
|
|
1877
1877
|
gateway=$(route -n get default 2>/dev/null | awk '/gateway:/{print $2}')
|
|
1878
1878
|
[[ -z "$gateway" ]] && return 1
|
|
1879
1879
|
|
|
1880
|
-
#
|
|
1880
|
+
# check if direct route exists
|
|
1881
1881
|
local current_gw
|
|
1882
1882
|
current_gw=$(route -n get "$proxy_ip" 2>/dev/null | awk '/gateway:/{print $2}')
|
|
1883
1883
|
[[ "$current_gw" == "$gateway" ]] && return 0
|
|
1884
1884
|
|
|
1885
|
-
echo "
|
|
1885
|
+
echo " adding direct route: $proxy_ip -> $gateway (needs sudo)"
|
|
1886
1886
|
sudo route add -host "$proxy_ip" "$gateway" >/dev/null 2>&1 || return 1
|
|
1887
1887
|
echo "$proxy_ip" > "$CAC_DIR/relay_route_ip"
|
|
1888
1888
|
|
|
@@ -1892,7 +1892,7 @@ _relay_add_route() {
|
|
|
1892
1892
|
iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
|
1893
1893
|
[[ -z "$gateway" ]] && return 1
|
|
1894
1894
|
|
|
1895
|
-
echo "
|
|
1895
|
+
echo " adding direct route: $proxy_ip -> $gateway dev $iface (needs sudo)"
|
|
1896
1896
|
sudo ip route add "$proxy_ip/32" via "$gateway" dev "$iface" 2>/dev/null || return 1
|
|
1897
1897
|
echo "$proxy_ip" > "$CAC_DIR/relay_route_ip"
|
|
1898
1898
|
fi
|
|
@@ -1914,7 +1914,7 @@ _relay_remove_route() {
|
|
|
1914
1914
|
rm -f "$route_file"
|
|
1915
1915
|
}
|
|
1916
1916
|
|
|
1917
|
-
#
|
|
1917
|
+
# detect if TUN interface is active
|
|
1918
1918
|
_detect_tun_active() {
|
|
1919
1919
|
local os; os=$(_detect_os)
|
|
1920
1920
|
if [[ "$os" == "macos" ]]; then
|
|
@@ -1928,12 +1928,12 @@ _detect_tun_active() {
|
|
|
1928
1928
|
fi
|
|
1929
1929
|
}
|
|
1930
1930
|
|
|
1931
|
-
# ──
|
|
1931
|
+
# ── user commands ─────────────────────────────────────────────────────
|
|
1932
1932
|
|
|
1933
1933
|
cmd_relay() {
|
|
1934
1934
|
_require_setup
|
|
1935
1935
|
local current; current=$(_current_env)
|
|
1936
|
-
[[ -z "$current" ]] && { echo "
|
|
1936
|
+
[[ -z "$current" ]] && { echo "error: no active environment, run 'cac <name>' first" >&2; exit 1; }
|
|
1937
1937
|
|
|
1938
1938
|
local env_dir="$ENVS_DIR/$current"
|
|
1939
1939
|
local action="${1:-status}"
|
|
@@ -1942,60 +1942,60 @@ cmd_relay() {
|
|
|
1942
1942
|
case "$action" in
|
|
1943
1943
|
on)
|
|
1944
1944
|
echo "on" > "$env_dir/relay"
|
|
1945
|
-
echo "$(_green "✓") Relay
|
|
1945
|
+
echo "$(_green "✓") Relay enabled (env: $(_bold "$current"))"
|
|
1946
1946
|
|
|
1947
|
-
# --route
|
|
1947
|
+
# --route flag: add direct route
|
|
1948
1948
|
if [[ "$flag" == "--route" ]]; then
|
|
1949
1949
|
local proxy; proxy=$(_read "$env_dir/proxy")
|
|
1950
1950
|
_relay_add_route "$proxy"
|
|
1951
1951
|
fi
|
|
1952
1952
|
|
|
1953
|
-
#
|
|
1953
|
+
# start relay if not running
|
|
1954
1954
|
if ! _relay_is_running; then
|
|
1955
|
-
printf "
|
|
1955
|
+
printf " starting relay ... "
|
|
1956
1956
|
if _relay_start "$current"; then
|
|
1957
1957
|
local port; port=$(_read "$CAC_DIR/relay.port")
|
|
1958
1958
|
echo "$(_green "✓") 127.0.0.1:$port"
|
|
1959
1959
|
else
|
|
1960
|
-
echo "$(_red "✗
|
|
1960
|
+
echo "$(_red "✗ failed to start")"
|
|
1961
1961
|
fi
|
|
1962
1962
|
fi
|
|
1963
|
-
echo "
|
|
1963
|
+
echo " next claude launch will automatically connect via local relay"
|
|
1964
1964
|
;;
|
|
1965
1965
|
off)
|
|
1966
1966
|
rm -f "$env_dir/relay"
|
|
1967
1967
|
_relay_stop
|
|
1968
|
-
echo "$(_green "✓") Relay
|
|
1968
|
+
echo "$(_green "✓") Relay disabled (env: $(_bold "$current"))"
|
|
1969
1969
|
;;
|
|
1970
1970
|
status)
|
|
1971
1971
|
if [[ -f "$env_dir/relay" ]] && [[ "$(_read "$env_dir/relay")" == "on" ]]; then
|
|
1972
|
-
echo "Relay
|
|
1972
|
+
echo "Relay mode: $(_green "enabled")"
|
|
1973
1973
|
else
|
|
1974
|
-
echo "Relay
|
|
1974
|
+
echo "Relay mode: disabled"
|
|
1975
1975
|
if _detect_tun_active; then
|
|
1976
|
-
echo " $(_yellow "⚠")
|
|
1976
|
+
echo " $(_yellow "⚠") TUN mode detected, consider running 'cac relay on'"
|
|
1977
1977
|
fi
|
|
1978
1978
|
return
|
|
1979
1979
|
fi
|
|
1980
1980
|
|
|
1981
1981
|
if _relay_is_running; then
|
|
1982
1982
|
local pid; pid=$(_read "$CAC_DIR/relay.pid")
|
|
1983
|
-
local port; port=$(_read "$CAC_DIR/relay.port" "
|
|
1984
|
-
echo "Relay
|
|
1983
|
+
local port; port=$(_read "$CAC_DIR/relay.port" "unknown")
|
|
1984
|
+
echo "Relay process: $(_green "running") (PID=$pid, port=$port)"
|
|
1985
1985
|
else
|
|
1986
|
-
echo "Relay
|
|
1986
|
+
echo "Relay process: $(_yellow "not started") (will auto-start with claude)"
|
|
1987
1987
|
fi
|
|
1988
1988
|
|
|
1989
1989
|
if [[ -f "$CAC_DIR/relay_route_ip" ]]; then
|
|
1990
1990
|
local route_ip; route_ip=$(_read "$CAC_DIR/relay_route_ip")
|
|
1991
|
-
echo "
|
|
1991
|
+
echo "Direct route: $route_ip"
|
|
1992
1992
|
fi
|
|
1993
1993
|
;;
|
|
1994
1994
|
*)
|
|
1995
|
-
echo "
|
|
1996
|
-
echo " on [--route]
|
|
1997
|
-
echo " off
|
|
1998
|
-
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
|
|
1999
1999
|
;;
|
|
2000
2000
|
esac
|
|
2001
2001
|
}
|
|
@@ -2199,17 +2199,17 @@ cmd_stop() {
|
|
|
2199
2199
|
|
|
2200
2200
|
cmd_continue() {
|
|
2201
2201
|
if [[ ! -f "$CAC_DIR/stopped" ]]; then
|
|
2202
|
-
echo "cac
|
|
2202
|
+
echo "cac is not stopped, no need to resume"
|
|
2203
2203
|
return
|
|
2204
2204
|
fi
|
|
2205
2205
|
|
|
2206
2206
|
local current; current=$(_current_env)
|
|
2207
2207
|
if [[ -z "$current" ]]; then
|
|
2208
|
-
echo "
|
|
2208
|
+
echo "error: no active environment, run 'cac <name>'" >&2; exit 1
|
|
2209
2209
|
fi
|
|
2210
2210
|
|
|
2211
2211
|
rm -f "$CAC_DIR/stopped"
|
|
2212
|
-
echo "$(_green "✓") cac
|
|
2212
|
+
echo "$(_green "✓") cac resumed — current env: $(_bold "$current")"
|
|
2213
2213
|
}
|
|
2214
2214
|
|
|
2215
2215
|
# ━━━ cmd_claude.sh ━━━
|
|
@@ -2939,7 +2939,7 @@ cmd_docker() {
|
|
|
2939
2939
|
}
|
|
2940
2940
|
|
|
2941
2941
|
# ━━━ cmd_delete.sh ━━━
|
|
2942
|
-
# ── cmd: delete
|
|
2942
|
+
# ── cmd: delete (uninstall) ────────────────────────────────────────
|
|
2943
2943
|
|
|
2944
2944
|
cmd_delete() {
|
|
2945
2945
|
echo "=== cac delete ==="
|
|
@@ -2950,50 +2950,50 @@ cmd_delete() {
|
|
|
2950
2950
|
|
|
2951
2951
|
_remove_path_from_rc "$rc_file"
|
|
2952
2952
|
|
|
2953
|
-
#
|
|
2953
|
+
# stop relay processes and routes
|
|
2954
2954
|
if [[ -d "$CAC_DIR" ]]; then
|
|
2955
2955
|
_relay_stop 2>/dev/null || true
|
|
2956
2956
|
|
|
2957
|
-
#
|
|
2957
|
+
# stop docker port-forward processes
|
|
2958
2958
|
if [[ -d /tmp/cac-docker-ports ]]; then
|
|
2959
2959
|
for _pf in /tmp/cac-docker-ports/*.pid; do
|
|
2960
2960
|
[[ -f "$_pf" ]] || continue
|
|
2961
2961
|
kill "$(cat "$_pf")" 2>/dev/null || true
|
|
2962
2962
|
rm -f "$_pf"
|
|
2963
2963
|
done
|
|
2964
|
-
echo " ✓
|
|
2964
|
+
echo " ✓ stopped docker port-forward processes"
|
|
2965
2965
|
fi
|
|
2966
2966
|
|
|
2967
|
-
#
|
|
2967
|
+
# fallback: clean up orphaned relay processes
|
|
2968
2968
|
pkill -f "node.*\.cac/relay\.js" 2>/dev/null || true
|
|
2969
2969
|
|
|
2970
2970
|
rm -rf "$CAC_DIR"
|
|
2971
|
-
echo " ✓
|
|
2971
|
+
echo " ✓ deleted $CAC_DIR"
|
|
2972
2972
|
else
|
|
2973
|
-
echo " - $CAC_DIR
|
|
2973
|
+
echo " - $CAC_DIR does not exist, skipping"
|
|
2974
2974
|
fi
|
|
2975
2975
|
|
|
2976
2976
|
local method
|
|
2977
2977
|
method=$(_install_method)
|
|
2978
2978
|
echo
|
|
2979
2979
|
if [[ "$method" == "npm" ]]; then
|
|
2980
|
-
echo " ✓
|
|
2980
|
+
echo " ✓ cleared all cac data and config"
|
|
2981
2981
|
echo
|
|
2982
|
-
echo "
|
|
2982
|
+
echo "to fully uninstall the cac command, run:"
|
|
2983
2983
|
echo " npm uninstall -g claude-cac"
|
|
2984
2984
|
else
|
|
2985
2985
|
if [[ -f "$HOME/bin/cac" ]]; then
|
|
2986
2986
|
rm -f "$HOME/bin/cac"
|
|
2987
|
-
echo " ✓
|
|
2987
|
+
echo " ✓ deleted $HOME/bin/cac"
|
|
2988
2988
|
fi
|
|
2989
|
-
echo " ✓
|
|
2989
|
+
echo " ✓ uninstall complete"
|
|
2990
2990
|
fi
|
|
2991
2991
|
|
|
2992
2992
|
echo
|
|
2993
2993
|
if [[ -n "$rc_file" ]]; then
|
|
2994
|
-
echo "
|
|
2994
|
+
echo "please restart terminal or run source $rc_file for changes to take effect."
|
|
2995
2995
|
else
|
|
2996
|
-
echo "
|
|
2996
|
+
echo "please restart terminal for changes to take effect."
|
|
2997
2997
|
fi
|
|
2998
2998
|
}
|
|
2999
2999
|
|
|
@@ -3045,7 +3045,7 @@ cmd_help() {
|
|
|
3045
3045
|
}
|
|
3046
3046
|
|
|
3047
3047
|
# ━━━ main.sh ━━━
|
|
3048
|
-
# ──
|
|
3048
|
+
# ── entry: dispatch commands ──────────────────────────────────────────────
|
|
3049
3049
|
|
|
3050
3050
|
[[ $# -eq 0 ]] && { cmd_help; exit 0; }
|
|
3051
3051
|
|