claude-cac 1.4.4 → 1.5.0-beta.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 +230 -62
- package/cac-dns-guard.js +1 -0
- package/fingerprint-hook.js +102 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +38 -0
package/cac
CHANGED
|
@@ -11,7 +11,7 @@ VERSIONS_DIR="$CAC_DIR/versions"
|
|
|
11
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.
|
|
14
|
+
CAC_VERSION="1.5.0-beta.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; }
|
|
@@ -61,6 +61,9 @@ _new_user_id() { python3 -c "import os; print(os.urandom(32).hex())" || _die "py
|
|
|
61
61
|
_new_machine_id() { _gen_uuid | tr -d '-' | tr '[:upper:]' '[:lower:]'; }
|
|
62
62
|
_new_hostname() { echo "host-$(_gen_uuid | cut -d- -f1 | tr '[:upper:]' '[:lower:]')"; }
|
|
63
63
|
_new_mac() { printf '02:%02x:%02x:%02x:%02x:%02x' $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)); }
|
|
64
|
+
_new_git_remote() { echo "https://github.com/user-$(_gen_uuid | cut -d- -f1)/project-$(_gen_uuid | cut -d- -f2).git"; }
|
|
65
|
+
_new_git_email() { echo "user-$(_gen_uuid | cut -d- -f1 | tr '[:upper:]' '[:lower:]')@users.noreply.github.com"; }
|
|
66
|
+
_new_device_token() { python3 -c "import os; print(os.urandom(32).hex())" || _die "python3 required"; }
|
|
64
67
|
|
|
65
68
|
# Get real command path (bypass shim)
|
|
66
69
|
_get_real_cmd() {
|
|
@@ -436,6 +439,7 @@ TELEMETRY_DOMAINS=(
|
|
|
436
439
|
"sentry.io"
|
|
437
440
|
"o1137031.ingest.sentry.io"
|
|
438
441
|
"cdn.growthbook.io"
|
|
442
|
+
"http-intake.logs.us5.datadoghq.com"
|
|
439
443
|
)
|
|
440
444
|
|
|
441
445
|
# write HOSTALIASES file (fallback layer: gethostbyname-level blocking)
|
|
@@ -476,6 +480,7 @@ var BLOCKED_DOMAINS = new Set([
|
|
|
476
480
|
'sentry.io',
|
|
477
481
|
'o1137031.ingest.sentry.io',
|
|
478
482
|
'cdn.growthbook.io',
|
|
483
|
+
'http-intake.logs.us5.datadoghq.com',
|
|
479
484
|
]);
|
|
480
485
|
|
|
481
486
|
/**
|
|
@@ -1173,42 +1178,51 @@ fi
|
|
|
1173
1178
|
export PATH="$CAC_DIR/shim-bin:$PATH"
|
|
1174
1179
|
|
|
1175
1180
|
# ── multi-layer telemetry protection ──
|
|
1176
|
-
|
|
1181
|
+
# Modes: stealth (default) | paranoid | transparent
|
|
1182
|
+
# stealth: only DISABLE_TELEMETRY=1 — 1p_events blocked, GrowthBook/Statsig/Feature flags normal
|
|
1183
|
+
# looks like a normal user; all fingerprints are fake so telemetry data is useless
|
|
1184
|
+
# paranoid: full 12-layer telemetry kill — zero telemetry (detectable as "anti-telemetry user")
|
|
1185
|
+
# transparent: no intervention — for when fingerprint coverage is complete
|
|
1186
|
+
# Backward compat: conservative→stealth, aggressive→paranoid, off→transparent
|
|
1187
|
+
_telemetry_mode="stealth"
|
|
1177
1188
|
[[ -f "$_env_dir/telemetry_mode" ]] && _telemetry_mode=$(tr -d '[:space:]' < "$_env_dir/telemetry_mode")
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1189
|
+
case "$_telemetry_mode" in
|
|
1190
|
+
conservative) _telemetry_mode="stealth" ;;
|
|
1191
|
+
aggressive) _telemetry_mode="paranoid" ;;
|
|
1192
|
+
off) _telemetry_mode="transparent" ;;
|
|
1193
|
+
esac
|
|
1194
|
+
if [[ "$_telemetry_mode" != "stealth" ]] && [[ "$_telemetry_mode" != "paranoid" ]] && [[ "$_telemetry_mode" != "transparent" ]]; then
|
|
1195
|
+
echo "[cac] warning: unknown telemetry mode '$_telemetry_mode', using stealth" >&2
|
|
1196
|
+
_telemetry_mode="stealth"
|
|
1185
1197
|
fi
|
|
1186
1198
|
|
|
1187
|
-
if [[ "$_telemetry_mode"
|
|
1188
|
-
|
|
1189
|
-
|
|
1199
|
+
if [[ "$_telemetry_mode" == "stealth" ]]; then
|
|
1200
|
+
# Block 1p_event reporting only; GrowthBook/Statsig/Feature flags work normally
|
|
1201
|
+
# Behavior indistinguishable from normal user — feature flags, fast mode etc. all enabled
|
|
1202
|
+
export DISABLE_TELEMETRY=1
|
|
1203
|
+
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=
|
|
1190
1204
|
fi
|
|
1191
1205
|
|
|
1192
|
-
if [[ "$_telemetry_mode" == "
|
|
1193
|
-
#
|
|
1206
|
+
if [[ "$_telemetry_mode" == "paranoid" ]]; then
|
|
1207
|
+
# Full 12-layer telemetry kill — zero telemetry + zero auxiliary requests
|
|
1208
|
+
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
|
1209
|
+
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=
|
|
1194
1210
|
export CLAUDE_CODE_ENABLE_TELEMETRY=
|
|
1195
|
-
# Layer 2: universal telemetry opt-out (https://consoledonottrack.com)
|
|
1196
1211
|
export DO_NOT_TRACK=1
|
|
1197
|
-
# Layer 3: OpenTelemetry SDK fully disabled
|
|
1198
1212
|
export OTEL_SDK_DISABLED=true
|
|
1199
1213
|
export OTEL_TRACES_EXPORTER=none
|
|
1200
1214
|
export OTEL_METRICS_EXPORTER=none
|
|
1201
1215
|
export OTEL_LOGS_EXPORTER=none
|
|
1202
|
-
# Layer 4: empty Sentry DSN, block error reporting
|
|
1203
1216
|
export SENTRY_DSN=
|
|
1204
|
-
# Layer 5: Claude Code specific toggles
|
|
1205
1217
|
export DISABLE_ERROR_REPORTING=1
|
|
1206
1218
|
export DISABLE_BUG_COMMAND=1
|
|
1207
|
-
# Layer 6: other known telemetry flags
|
|
1208
1219
|
export TELEMETRY_DISABLED=1
|
|
1209
1220
|
export DISABLE_TELEMETRY=1
|
|
1210
1221
|
fi
|
|
1211
1222
|
|
|
1223
|
+
# ── billing header suppression (x-anthropic-billing-header) ──
|
|
1224
|
+
export CLAUDE_CODE_ATTRIBUTION_HEADER=0
|
|
1225
|
+
|
|
1212
1226
|
# with proxy: force OAuth (clear API config to prevent leaks)
|
|
1213
1227
|
# without proxy: preserve user's API Key / Base URL
|
|
1214
1228
|
if [[ -n "$PROXY" ]]; then
|
|
@@ -1217,6 +1231,58 @@ if [[ -n "$PROXY" ]]; then
|
|
|
1217
1231
|
unset ANTHROPIC_API_KEY
|
|
1218
1232
|
fi
|
|
1219
1233
|
|
|
1234
|
+
# ── git identity spoofing ──
|
|
1235
|
+
# Prevent git email leakage (Claude runs `git config --get user.email` on startup)
|
|
1236
|
+
if [[ -f "$_env_dir/git_email" ]]; then
|
|
1237
|
+
_git_email=$(tr -d '[:space:]' < "$_env_dir/git_email")
|
|
1238
|
+
export GIT_AUTHOR_EMAIL="$_git_email"
|
|
1239
|
+
export GIT_COMMITTER_EMAIL="$_git_email"
|
|
1240
|
+
export CAC_GIT_EMAIL="$_git_email"
|
|
1241
|
+
fi
|
|
1242
|
+
|
|
1243
|
+
# ── repository fingerprint (rh) spoofing ──
|
|
1244
|
+
# Claude computes rh=SHA256(git_remote_url) per event — cross-account linkage vector
|
|
1245
|
+
if [[ -f "$_env_dir/fake_git_remote" ]]; then
|
|
1246
|
+
export CAC_FAKE_GIT_REMOTE=$(tr -d '[:space:]' < "$_env_dir/fake_git_remote")
|
|
1247
|
+
fi
|
|
1248
|
+
|
|
1249
|
+
# ── Trusted Device Token (preemptive) ──
|
|
1250
|
+
# tengu_sessions_elevated_auth_enforcement gate is currently off but mechanism is ready
|
|
1251
|
+
if [[ -f "$_env_dir/device_token" ]]; then
|
|
1252
|
+
export CLAUDE_TRUSTED_DEVICE_TOKEN=$(tr -d '[:space:]' < "$_env_dir/device_token")
|
|
1253
|
+
fi
|
|
1254
|
+
|
|
1255
|
+
# ── persona (Docker/server environment spoofing) ──
|
|
1256
|
+
if [[ -f "$_env_dir/persona" ]]; then
|
|
1257
|
+
_persona=$(tr -d '[:space:]' < "$_env_dir/persona")
|
|
1258
|
+
case "$_persona" in
|
|
1259
|
+
macos-vscode)
|
|
1260
|
+
export TERM_PROGRAM="vscode"
|
|
1261
|
+
export VSCODE_GIT_ASKPASS_MAIN="/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass-main.js"
|
|
1262
|
+
export __CFBundleIdentifier="com.microsoft.VSCode"
|
|
1263
|
+
export TERM="xterm-256color"
|
|
1264
|
+
;;
|
|
1265
|
+
macos-cursor)
|
|
1266
|
+
export TERM_PROGRAM="vscode"
|
|
1267
|
+
export CURSOR_TRACE_ID="cursor-$(head -c 8 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
|
1268
|
+
export __CFBundleIdentifier="com.todesktop.230313mzl4w4u92"
|
|
1269
|
+
export TERM="xterm-256color"
|
|
1270
|
+
;;
|
|
1271
|
+
macos-iterm)
|
|
1272
|
+
export TERM_PROGRAM="iTerm.app"
|
|
1273
|
+
export __CFBundleIdentifier="com.googlecode.iterm2"
|
|
1274
|
+
export TERM="xterm-256color"
|
|
1275
|
+
export ITERM_SESSION_ID="w0t0p0:$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
|
1276
|
+
;;
|
|
1277
|
+
linux-desktop)
|
|
1278
|
+
export TERM_PROGRAM="vscode"
|
|
1279
|
+
export TERM="xterm-256color"
|
|
1280
|
+
;;
|
|
1281
|
+
esac
|
|
1282
|
+
# Hide Docker signals when persona is active
|
|
1283
|
+
export CAC_HIDE_DOCKER=1
|
|
1284
|
+
fi
|
|
1285
|
+
|
|
1220
1286
|
# ── NS-level DNS interception ──
|
|
1221
1287
|
if [[ -f "$CAC_DIR/cac-dns-guard.js" ]]; then
|
|
1222
1288
|
case "${NODE_OPTIONS:-}" in
|
|
@@ -1542,7 +1608,7 @@ _ensure_initialized() {
|
|
|
1542
1608
|
|
|
1543
1609
|
_env_cmd_create() {
|
|
1544
1610
|
_require_setup
|
|
1545
|
-
local name="" proxy="" claude_ver="" env_type="local" telemetry_mode="" clone_source="" clone_link=true
|
|
1611
|
+
local name="" proxy="" claude_ver="" env_type="local" telemetry_mode="" clone_source="" clone_link=true persona=""
|
|
1546
1612
|
|
|
1547
1613
|
while [[ $# -gt 0 ]]; do
|
|
1548
1614
|
case "$1" in
|
|
@@ -1550,7 +1616,15 @@ _env_cmd_create() {
|
|
|
1550
1616
|
-c|--claude) [[ $# -ge 2 ]] || _die "$1 requires a value"; claude_ver="$2"; shift 2 ;;
|
|
1551
1617
|
--type) [[ $# -ge 2 ]] || _die "$1 requires a value"; env_type="$2"; shift 2 ;;
|
|
1552
1618
|
--telemetry) [[ $# -ge 2 ]] || _die "$1 requires a value"; telemetry_mode="$2"; shift 2
|
|
1553
|
-
|
|
1619
|
+
# Accept both old and new names
|
|
1620
|
+
case "$telemetry_mode" in
|
|
1621
|
+
conservative) telemetry_mode="stealth" ;;
|
|
1622
|
+
aggressive) telemetry_mode="paranoid" ;;
|
|
1623
|
+
off) telemetry_mode="transparent" ;;
|
|
1624
|
+
esac
|
|
1625
|
+
[[ "$telemetry_mode" =~ ^(stealth|paranoid|transparent)$ ]] || _die "invalid telemetry mode '$telemetry_mode' (use stealth, paranoid, or transparent)" ;;
|
|
1626
|
+
--persona) [[ $# -ge 2 ]] || _die "$1 requires a value"; persona="$2"; shift 2
|
|
1627
|
+
[[ "$persona" =~ ^(macos-vscode|macos-cursor|macos-iterm|linux-desktop)$ ]] || _die "invalid persona '$persona' (use macos-vscode, macos-cursor, macos-iterm, or linux-desktop)" ;;
|
|
1554
1628
|
--clone) shift; if [[ -n "${1:-}" ]] && [[ "${1:-}" != -* ]]; then clone_source="$1"; shift; else clone_source="host"; fi ;;
|
|
1555
1629
|
--no-link) clone_link=false; shift ;;
|
|
1556
1630
|
-*) _die "unknown option: $1" ;;
|
|
@@ -1636,10 +1710,14 @@ _env_cmd_create() {
|
|
|
1636
1710
|
echo "$lang" > "$env_dir/lang"
|
|
1637
1711
|
[[ -n "$claude_ver" ]] && echo "$claude_ver" > "$env_dir/version"
|
|
1638
1712
|
echo "$env_type" > "$env_dir/type"
|
|
1713
|
+
echo "$(_new_git_remote)" > "$env_dir/fake_git_remote"
|
|
1714
|
+
echo "$(_new_git_email)" > "$env_dir/git_email"
|
|
1715
|
+
echo "$(_new_device_token)" > "$env_dir/device_token"
|
|
1639
1716
|
date -u +"%Y-%m-%dT%H:%M:%S.000Z" > "$env_dir/first_start_time"
|
|
1717
|
+
[[ -n "$persona" ]] && echo "$persona" > "$env_dir/persona"
|
|
1640
1718
|
|
|
1641
|
-
# Telemetry mode:
|
|
1642
|
-
[[ -z "$telemetry_mode" ]] && telemetry_mode=$(_cac_setting telemetry_mode
|
|
1719
|
+
# Telemetry mode: stealth (default), paranoid, or transparent
|
|
1720
|
+
[[ -z "$telemetry_mode" ]] && telemetry_mode=$(_cac_setting telemetry_mode stealth)
|
|
1643
1721
|
echo "$telemetry_mode" > "$env_dir/telemetry_mode"
|
|
1644
1722
|
|
|
1645
1723
|
mkdir -p "$env_dir/.claude"
|
|
@@ -1836,15 +1914,17 @@ _env_cmd_set() {
|
|
|
1836
1914
|
# Parse: cac env set [name] <key> <value|--remove>
|
|
1837
1915
|
# If first arg is a known key, use current env; otherwise treat as env name
|
|
1838
1916
|
local name="" key="" value="" remove=false
|
|
1839
|
-
local known_keys="proxy version"
|
|
1917
|
+
local known_keys="proxy version telemetry persona"
|
|
1840
1918
|
|
|
1841
1919
|
if [[ $# -lt 1 ]] || [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "help" ]]; then
|
|
1842
1920
|
echo
|
|
1843
1921
|
echo " $(_bold "cac env set") — modify environment configuration"
|
|
1844
1922
|
echo
|
|
1845
|
-
echo " $(_green "set") [name] proxy <url>
|
|
1846
|
-
echo " $(_green "set") [name] proxy --remove
|
|
1847
|
-
echo " $(_green "set") [name] version <ver|latest>
|
|
1923
|
+
echo " $(_green "set") [name] proxy <url> Set proxy"
|
|
1924
|
+
echo " $(_green "set") [name] proxy --remove Remove proxy"
|
|
1925
|
+
echo " $(_green "set") [name] version <ver|latest> Change Claude version"
|
|
1926
|
+
echo " $(_green "set") [name] telemetry <stealth|paranoid|transparent>"
|
|
1927
|
+
echo " $(_green "set") [name] persona <macos-vscode|macos-cursor|macos-iterm|linux-desktop|--remove>"
|
|
1848
1928
|
echo
|
|
1849
1929
|
echo " $(_dim "If name is omitted, uses the current active environment.")"
|
|
1850
1930
|
echo
|
|
@@ -1902,8 +1982,32 @@ _env_cmd_set() {
|
|
|
1902
1982
|
echo "$ver" > "$env_dir/version"
|
|
1903
1983
|
echo "$(_green_bold "Set") version for $(_bold "$name") → $(_cyan "$ver")"
|
|
1904
1984
|
;;
|
|
1985
|
+
telemetry)
|
|
1986
|
+
[[ "$remove" != "true" ]] || _die "cannot remove telemetry mode"
|
|
1987
|
+
[[ -n "$value" ]] || _die "usage: cac env set [name] telemetry <stealth|paranoid|transparent>"
|
|
1988
|
+
# Accept old names
|
|
1989
|
+
case "$value" in
|
|
1990
|
+
conservative) value="stealth" ;;
|
|
1991
|
+
aggressive) value="paranoid" ;;
|
|
1992
|
+
off) value="transparent" ;;
|
|
1993
|
+
esac
|
|
1994
|
+
[[ "$value" =~ ^(stealth|paranoid|transparent)$ ]] || _die "invalid telemetry mode '$value' (use stealth, paranoid, or transparent)"
|
|
1995
|
+
echo "$value" > "$env_dir/telemetry_mode"
|
|
1996
|
+
echo "$(_green_bold "Set") telemetry for $(_bold "$name") → $(_cyan "$value")"
|
|
1997
|
+
;;
|
|
1998
|
+
persona)
|
|
1999
|
+
if [[ "$remove" == "true" ]]; then
|
|
2000
|
+
rm -f "$env_dir/persona"
|
|
2001
|
+
echo "$(_green_bold "Removed") persona from $(_bold "$name")"
|
|
2002
|
+
else
|
|
2003
|
+
[[ -n "$value" ]] || _die "usage: cac env set [name] persona <macos-vscode|macos-cursor|macos-iterm|linux-desktop>"
|
|
2004
|
+
[[ "$value" =~ ^(macos-vscode|macos-cursor|macos-iterm|linux-desktop)$ ]] || _die "invalid persona '$value'"
|
|
2005
|
+
echo "$value" > "$env_dir/persona"
|
|
2006
|
+
echo "$(_green_bold "Set") persona for $(_bold "$name") → $(_cyan "$value")"
|
|
2007
|
+
fi
|
|
2008
|
+
;;
|
|
1905
2009
|
*)
|
|
1906
|
-
_die "unknown key '$key' — use proxy or
|
|
2010
|
+
_die "unknown key '$key' — use proxy, version, telemetry, or persona"
|
|
1907
2011
|
;;
|
|
1908
2012
|
esac
|
|
1909
2013
|
}
|
|
@@ -1921,7 +2025,7 @@ cmd_env() {
|
|
|
1921
2025
|
echo
|
|
1922
2026
|
echo " $(_bold "cac env") — environment management"
|
|
1923
2027
|
echo
|
|
1924
|
-
echo " $(_green "create") <name> [-p proxy] [-c ver] [--clone [source]] [--no-link] [--telemetry mode]"
|
|
2028
|
+
echo " $(_green "create") <name> [-p proxy] [-c ver] [--clone [source]] [--no-link] [--telemetry mode] [--persona preset]"
|
|
1925
2029
|
echo " $(_green "set") [name] proxy <url> Set proxy"
|
|
1926
2030
|
echo " $(_green "set") [name] proxy --remove Remove proxy"
|
|
1927
2031
|
echo " $(_green "set") [name] version <ver|latest> Change Claude version"
|
|
@@ -2210,57 +2314,111 @@ cmd_check() {
|
|
|
2210
2314
|
local wrapper_file="$CAC_DIR/bin/claude"
|
|
2211
2315
|
local wrapper_content=""
|
|
2212
2316
|
[[ -f "$wrapper_file" ]] && wrapper_content=$(<"$wrapper_file")
|
|
2213
|
-
local telemetry_mode; telemetry_mode=$(_read "$env_dir/telemetry_mode" "
|
|
2214
|
-
|
|
2215
|
-
|
|
2317
|
+
local telemetry_mode; telemetry_mode=$(_read "$env_dir/telemetry_mode" "stealth")
|
|
2318
|
+
# Normalize old names
|
|
2319
|
+
case "$telemetry_mode" in conservative) telemetry_mode="stealth" ;; aggressive) telemetry_mode="paranoid" ;; off) telemetry_mode="transparent" ;; esac
|
|
2320
|
+
local _tel_stealth_vars=("DISABLE_TELEMETRY" "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA")
|
|
2321
|
+
local _tel_paranoid_vars=(
|
|
2216
2322
|
"CLAUDE_CODE_ENABLE_TELEMETRY" "DO_NOT_TRACK"
|
|
2217
2323
|
"OTEL_SDK_DISABLED" "OTEL_TRACES_EXPORTER" "OTEL_METRICS_EXPORTER" "OTEL_LOGS_EXPORTER"
|
|
2218
2324
|
"SENTRY_DSN" "DISABLE_ERROR_REPORTING" "DISABLE_BUG_COMMAND"
|
|
2219
2325
|
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC" "TELEMETRY_DISABLED" "DISABLE_TELEMETRY"
|
|
2220
2326
|
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA"
|
|
2221
2327
|
)
|
|
2222
|
-
if [[ "$telemetry_mode" == "
|
|
2223
|
-
echo " $(_dim "○") telemetry
|
|
2224
|
-
elif [[ "$telemetry_mode" == "
|
|
2225
|
-
local env_ok=0 env_total=${#
|
|
2226
|
-
for var in "${
|
|
2328
|
+
if [[ "$telemetry_mode" == "transparent" ]]; then
|
|
2329
|
+
echo " $(_dim "○") telemetry transparent (no protection)"
|
|
2330
|
+
elif [[ "$telemetry_mode" == "paranoid" ]]; then
|
|
2331
|
+
local env_ok=0 env_total=${#_tel_paranoid_vars[@]}
|
|
2332
|
+
for var in "${_tel_paranoid_vars[@]}"; do
|
|
2227
2333
|
[[ "$wrapper_content" == *"$var"* ]] && (( env_ok++ )) || true
|
|
2228
2334
|
done
|
|
2229
2335
|
if [[ "$env_ok" -eq "$env_total" ]]; then
|
|
2230
|
-
echo " $(_green "✓") telemetry
|
|
2336
|
+
echo " $(_green "✓") telemetry paranoid ${env_ok}/${env_total} blocked"
|
|
2231
2337
|
else
|
|
2232
|
-
echo " $(_red "✗") telemetry
|
|
2338
|
+
echo " $(_red "✗") telemetry paranoid ${env_ok}/${env_total} blocked"
|
|
2233
2339
|
problems+=("telemetry shield ${env_ok}/${env_total}")
|
|
2234
2340
|
fi
|
|
2235
2341
|
else
|
|
2236
|
-
local
|
|
2237
|
-
for var in "${
|
|
2238
|
-
[[ "$wrapper_content" == *"$var"* ]] && ((
|
|
2342
|
+
local stealth_ok=0
|
|
2343
|
+
for var in "${_tel_stealth_vars[@]}"; do
|
|
2344
|
+
[[ "$wrapper_content" == *"$var"* ]] && (( stealth_ok++ )) || true
|
|
2239
2345
|
done
|
|
2240
|
-
if [[ "$
|
|
2241
|
-
echo " $(_green "✓") telemetry
|
|
2346
|
+
if [[ "$stealth_ok" -eq 2 ]]; then
|
|
2347
|
+
echo " $(_green "✓") telemetry stealth (1p blocked, features normal)"
|
|
2242
2348
|
else
|
|
2243
|
-
echo " $(_red "✗") telemetry
|
|
2349
|
+
echo " $(_red "✗") telemetry stealth ($stealth_ok/2)"
|
|
2244
2350
|
problems+=("telemetry shield incomplete")
|
|
2245
2351
|
fi
|
|
2246
2352
|
fi
|
|
2247
2353
|
|
|
2248
|
-
# ──
|
|
2354
|
+
# ── identity spoofing (consolidated) ──
|
|
2355
|
+
local os; os=$(_detect_os)
|
|
2356
|
+
local _id_ok=0 _id_total=0 _id_issues=()
|
|
2357
|
+
|
|
2358
|
+
# fingerprint hook
|
|
2359
|
+
local _fp_ok=false
|
|
2249
2360
|
if [[ -f "$CAC_DIR/fingerprint-hook.js" ]] && [[ -f "$env_dir/hostname" ]]; then
|
|
2250
2361
|
local expected_hn; expected_hn=$(_read "$env_dir/hostname")
|
|
2251
2362
|
local actual_hn
|
|
2252
2363
|
actual_hn=$(NODE_OPTIONS="--require $CAC_DIR/fingerprint-hook.js" CAC_HOSTNAME="$expected_hn" \
|
|
2253
2364
|
node -e "process.stdout.write(require('os').hostname())" 2>/dev/null || true)
|
|
2365
|
+
(( _id_total++ )) || true
|
|
2254
2366
|
if [[ "$actual_hn" == "$expected_hn" ]]; then
|
|
2255
|
-
|
|
2367
|
+
_fp_ok=true; (( _id_ok++ )) || true
|
|
2256
2368
|
else
|
|
2257
|
-
|
|
2258
|
-
|
|
2369
|
+
_id_issues+=("fingerprint hook not working")
|
|
2370
|
+
fi
|
|
2371
|
+
fi
|
|
2372
|
+
# git email
|
|
2373
|
+
(( _id_total++ )) || true
|
|
2374
|
+
if [[ -f "$env_dir/git_email" ]]; then
|
|
2375
|
+
(( _id_ok++ )) || true
|
|
2376
|
+
else
|
|
2377
|
+
_id_issues+=("git email not spoofed")
|
|
2378
|
+
fi
|
|
2379
|
+
# repo hash
|
|
2380
|
+
(( _id_total++ )) || true
|
|
2381
|
+
if [[ -f "$env_dir/fake_git_remote" ]]; then
|
|
2382
|
+
(( _id_ok++ )) || true
|
|
2383
|
+
else
|
|
2384
|
+
_id_issues+=("repo hash not spoofed")
|
|
2385
|
+
fi
|
|
2386
|
+
# user_id consistency
|
|
2387
|
+
local _uid_ok=true
|
|
2388
|
+
local _env_uid; _env_uid=$(_read "$env_dir/user_id" "")
|
|
2389
|
+
if [[ -n "$_env_uid" ]]; then
|
|
2390
|
+
local _config_dir="${CLAUDE_CONFIG_DIR:-$ENVS_DIR/$current/.claude}"
|
|
2391
|
+
local _cj="$_config_dir/.claude.json"
|
|
2392
|
+
[[ -f "$_cj" ]] || _cj="$HOME/.claude.json"
|
|
2393
|
+
if [[ -f "$_cj" ]]; then
|
|
2394
|
+
local _actual_uid
|
|
2395
|
+
_actual_uid=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('userID',''))" "$_cj" 2>/dev/null || true)
|
|
2396
|
+
(( _id_total++ )) || true
|
|
2397
|
+
if [[ -n "$_actual_uid" ]] && [[ "$_actual_uid" != "$_env_uid" ]]; then
|
|
2398
|
+
_uid_ok=false
|
|
2399
|
+
_id_issues+=("user_id mismatch")
|
|
2400
|
+
else
|
|
2401
|
+
(( _id_ok++ )) || true
|
|
2402
|
+
fi
|
|
2259
2403
|
fi
|
|
2260
2404
|
fi
|
|
2405
|
+
# billing header
|
|
2406
|
+
(( _id_total++ )) || true
|
|
2407
|
+
[[ "$wrapper_content" == *"CLAUDE_CODE_ATTRIBUTION_HEADER"* ]] && { (( _id_ok++ )) || true; } || _id_issues+=("billing header exposed")
|
|
2408
|
+
|
|
2409
|
+
# display consolidated identity line
|
|
2410
|
+
local _id_extra=""
|
|
2411
|
+
[[ -f "$env_dir/persona" ]] && _id_extra=" + $(_read "$env_dir/persona")"
|
|
2412
|
+
if [[ "$_id_ok" -eq "$_id_total" ]]; then
|
|
2413
|
+
echo " $(_green "✓") identity ${_id_ok}/${_id_total} spoofed${_id_extra}"
|
|
2414
|
+
else
|
|
2415
|
+
echo " $(_red "✗") identity ${_id_ok}/${_id_total} spoofed${_id_extra}"
|
|
2416
|
+
for _ii in "${_id_issues[@]}"; do
|
|
2417
|
+
problems+=("$_ii")
|
|
2418
|
+
done
|
|
2419
|
+
fi
|
|
2261
2420
|
|
|
2262
2421
|
# ── IPv6 leak detection ──
|
|
2263
|
-
local os; os=$(_detect_os)
|
|
2264
2422
|
local ipv6_leak=false
|
|
2265
2423
|
if [[ "$os" == "macos" ]]; then
|
|
2266
2424
|
local ipv6_addrs
|
|
@@ -2272,27 +2430,28 @@ cmd_check() {
|
|
|
2272
2430
|
[[ "$ipv6_addrs" -gt 0 ]] && ipv6_leak=true
|
|
2273
2431
|
fi
|
|
2274
2432
|
if [[ "$ipv6_leak" == "true" ]]; then
|
|
2275
|
-
echo " $(_yellow "⚠") IPv6
|
|
2433
|
+
echo " $(_yellow "⚠") IPv6 global address detected (potential leak)"
|
|
2276
2434
|
else
|
|
2277
|
-
echo " $(_green "✓") IPv6
|
|
2435
|
+
echo " $(_green "✓") IPv6 no global address"
|
|
2278
2436
|
fi
|
|
2279
2437
|
|
|
2280
|
-
# ──
|
|
2438
|
+
# ── warnings (only shown when relevant) ──
|
|
2281
2439
|
if [[ -d "$HOME/.claude/telemetry" ]]; then
|
|
2282
2440
|
local tel_files
|
|
2283
2441
|
tel_files=$(find "$HOME/.claude/telemetry" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
2284
2442
|
if [[ "$tel_files" -gt 0 ]]; then
|
|
2285
|
-
echo " $(_yellow "⚠") residual
|
|
2286
|
-
echo " $(_dim "hint: rm -rf ~/.claude/telemetry/")"
|
|
2443
|
+
echo " $(_yellow "⚠") residual $tel_files telemetry files in ~/.claude/telemetry/"
|
|
2287
2444
|
fi
|
|
2288
2445
|
fi
|
|
2289
|
-
|
|
2290
|
-
# ── concurrent sessions ──
|
|
2291
2446
|
local _claude_count
|
|
2292
2447
|
_claude_count=$(pgrep -x "claude" 2>/dev/null | wc -l | tr -d '[:space:]') || _claude_count=0
|
|
2293
2448
|
local _max_sessions; _max_sessions=$(_cac_setting max_sessions 10)
|
|
2294
2449
|
if [[ "$_claude_count" -gt "$_max_sessions" ]]; then
|
|
2295
|
-
echo " $(_yellow "⚠") sessions
|
|
2450
|
+
echo " $(_yellow "⚠") sessions $_claude_count running (threshold: $_max_sessions)"
|
|
2451
|
+
fi
|
|
2452
|
+
if [[ "$os" == "macos" ]]; then
|
|
2453
|
+
security find-generic-password -s "claude-code-credentials" >/dev/null 2>&1 && \
|
|
2454
|
+
echo " $(_yellow "⚠") keychain Trusted Device Token residual"
|
|
2296
2455
|
fi
|
|
2297
2456
|
|
|
2298
2457
|
# ── network check (slow — streaming output) ──
|
|
@@ -2392,20 +2551,29 @@ cmd_check() {
|
|
|
2392
2551
|
|
|
2393
2552
|
# ── verbose mode ──
|
|
2394
2553
|
if [[ "$verbose" == "true" ]]; then
|
|
2554
|
+
echo " $(_bold "Identity")"
|
|
2555
|
+
echo " $([[ "$_fp_ok" == "true" ]] && _green "✓" || _red "✗") hostname $(_read "$env_dir/hostname" "—")"
|
|
2556
|
+
echo " $([[ -f "$env_dir/git_email" ]] && _green "✓" || _yellow "⚠") git email $(_read "$env_dir/git_email" "—")"
|
|
2557
|
+
echo " $([[ -f "$env_dir/fake_git_remote" ]] && _green "✓" || _yellow "⚠") repo hash $(_read "$env_dir/fake_git_remote" "—")"
|
|
2558
|
+
echo " $([[ "$_uid_ok" == "true" ]] && _green "✓" || _yellow "⚠") user_id $(_read "$env_dir/user_id" "—" | cut -c1-16)..."
|
|
2559
|
+
echo " $([[ "$wrapper_content" == *"CLAUDE_CODE_ATTRIBUTION_HEADER"* ]] && _green "✓" || _yellow "⚠") billing $([[ "$wrapper_content" == *"CLAUDE_CODE_ATTRIBUTION_HEADER"* ]] && echo "disabled" || echo "exposed")"
|
|
2560
|
+
[[ -f "$env_dir/persona" ]] && echo " $(_green "✓") persona $(_read "$env_dir/persona")"
|
|
2561
|
+
echo
|
|
2395
2562
|
echo " $(_bold "Details")"
|
|
2396
2563
|
echo " $(_dim "UUID") $(_read "$env_dir/uuid")"
|
|
2397
2564
|
echo " $(_dim "stable_id") $(_read "$env_dir/stable_id")"
|
|
2398
|
-
echo " $(_dim "
|
|
2565
|
+
echo " $(_dim "MAC") $(_read "$env_dir/mac_address" "—")"
|
|
2566
|
+
echo " $(_dim "machine_id") $(_read "$env_dir/machine_id" "—")"
|
|
2399
2567
|
echo " $(_dim "TZ") $(_read "$env_dir/tz" "—")"
|
|
2400
2568
|
echo " $(_dim "LANG") $(_read "$env_dir/lang" "—")"
|
|
2401
2569
|
echo " $(_dim "env") ${env_dir/#$HOME/~}/.claude/"
|
|
2402
2570
|
echo
|
|
2403
2571
|
echo " $(_bold "Telemetry") ($telemetry_mode mode)"
|
|
2404
|
-
if [[ "$telemetry_mode" == "
|
|
2572
|
+
if [[ "$telemetry_mode" == "transparent" ]]; then
|
|
2405
2573
|
echo " $(_dim " no telemetry protection active")"
|
|
2406
2574
|
fi
|
|
2407
|
-
local _vvars=("${
|
|
2408
|
-
[[ "$telemetry_mode" == "
|
|
2575
|
+
local _vvars=("${_tel_stealth_vars[@]}")
|
|
2576
|
+
[[ "$telemetry_mode" == "paranoid" ]] && _vvars=("${_tel_paranoid_vars[@]}")
|
|
2409
2577
|
for var in "${_vvars[@]}"; do
|
|
2410
2578
|
if [[ "$wrapper_content" == *"$var"* ]]; then
|
|
2411
2579
|
printf " $(_green "✓") %s\n" "$var"
|
package/cac-dns-guard.js
CHANGED
package/fingerprint-hook.js
CHANGED
|
@@ -97,6 +97,108 @@ if (fakeMachineId) {
|
|
|
97
97
|
} catch (_) { /* fs/promises not available on older Node */ }
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// --- Repository fingerprint (rh) interception ---
|
|
101
|
+
// Claude Code computes rh = SHA256(normalized_git_remote_url).hex.slice(0,16)
|
|
102
|
+
// and sends it with every 1p_event — cross-account linkage vector
|
|
103
|
+
const fakeGitRemote = process.env.CAC_FAKE_GIT_REMOTE;
|
|
104
|
+
if (fakeGitRemote) {
|
|
105
|
+
const GIT_REMOTE_PATTERNS = [
|
|
106
|
+
/git\s+remote\s+get-url/i,
|
|
107
|
+
/git\s+remote\s+-v/i,
|
|
108
|
+
/git\s+config\s+--get\s+remote\..*\.url/i,
|
|
109
|
+
/git\s+ls-remote\s+--get-url/i,
|
|
110
|
+
];
|
|
111
|
+
function isGitRemoteCmd(cmdStr) {
|
|
112
|
+
return GIT_REMOTE_PATTERNS.some(function(p) { return p.test(cmdStr); });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const _origExecSyncFp = child_process.execSync.bind(child_process);
|
|
116
|
+
child_process.execSync = function(cmd, options) {
|
|
117
|
+
var cmdStr = typeof cmd === 'string' ? cmd : cmd.toString();
|
|
118
|
+
if (isGitRemoteCmd(cmdStr)) {
|
|
119
|
+
var result = fakeGitRemote + '\n';
|
|
120
|
+
return (typeof options === 'string' || (options && options.encoding))
|
|
121
|
+
? result : Buffer.from(result);
|
|
122
|
+
}
|
|
123
|
+
return _origExecSyncFp(cmd, options);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const _origExecFp = child_process.exec.bind(child_process);
|
|
127
|
+
child_process.exec = function(cmd) {
|
|
128
|
+
var args = Array.prototype.slice.call(arguments);
|
|
129
|
+
var cmdStr = typeof cmd === 'string' ? cmd : cmd.toString();
|
|
130
|
+
var cb = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : null;
|
|
131
|
+
if (isGitRemoteCmd(cmdStr)) {
|
|
132
|
+
if (cb) process.nextTick(cb, null, fakeGitRemote + '\n', '');
|
|
133
|
+
var { EventEmitter } = require('events');
|
|
134
|
+
var cp = new EventEmitter();
|
|
135
|
+
cp.stdout = new EventEmitter(); cp.stderr = new EventEmitter();
|
|
136
|
+
cp.stdin = null; cp.pid = 0; cp.kill = function() { return false; };
|
|
137
|
+
return cp;
|
|
138
|
+
}
|
|
139
|
+
return _origExecFp.apply(child_process, args);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const _origExecFileSyncFp = child_process.execFileSync.bind(child_process);
|
|
143
|
+
child_process.execFileSync = function(file, argsOrOpts, options) {
|
|
144
|
+
var fileArgs = Array.isArray(argsOrOpts) ? argsOrOpts : [];
|
|
145
|
+
var fullCmd = file + ' ' + fileArgs.join(' ');
|
|
146
|
+
if (isGitRemoteCmd(fullCmd)) {
|
|
147
|
+
var opts = Array.isArray(argsOrOpts) ? options : argsOrOpts;
|
|
148
|
+
var result = fakeGitRemote + '\n';
|
|
149
|
+
return (typeof opts === 'string' || (opts && opts.encoding))
|
|
150
|
+
? result : Buffer.from(result);
|
|
151
|
+
}
|
|
152
|
+
return _origExecFileSyncFp(file, argsOrOpts, options);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Git email interception ---
|
|
157
|
+
// Claude Code runs `git config --get user.email` on startup (yM8 line 159222)
|
|
158
|
+
// Intercept to prevent real email leakage (wrapper also sets GIT_AUTHOR_EMAIL)
|
|
159
|
+
const fakeGitEmail = process.env.CAC_GIT_EMAIL;
|
|
160
|
+
if (fakeGitEmail) {
|
|
161
|
+
// Re-wrap execSync if not already wrapped for git remote
|
|
162
|
+
var _prevExecSync = child_process.execSync;
|
|
163
|
+
child_process.execSync = function(cmd, options) {
|
|
164
|
+
var cmdStr = typeof cmd === 'string' ? cmd : cmd.toString();
|
|
165
|
+
if (/git\s+config\s+(--global\s+|--get\s+)*user\.email/i.test(cmdStr)) {
|
|
166
|
+
var result = fakeGitEmail + '\n';
|
|
167
|
+
return (typeof options === 'string' || (options && options.encoding))
|
|
168
|
+
? result : Buffer.from(result);
|
|
169
|
+
}
|
|
170
|
+
return _prevExecSync(cmd, options);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- Docker/container environment detection bypass ---
|
|
175
|
+
// Claude Code checks /.dockerenv and /proc/1/cgroup to detect Docker
|
|
176
|
+
// In Docker mode with persona, we hide container signals
|
|
177
|
+
if (process.env.CAC_HIDE_DOCKER === '1') {
|
|
178
|
+
const _origExistsSync = fs.existsSync.bind(fs);
|
|
179
|
+
fs.existsSync = function(p) {
|
|
180
|
+
var ps = typeof p === 'string' ? p : (p && p.toString ? p.toString() : '');
|
|
181
|
+
if (ps === '/.dockerenv') return false;
|
|
182
|
+
return _origExistsSync(p);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Intercept /proc/1/cgroup reads to remove docker references
|
|
186
|
+
if (fakeMachineId || true) {
|
|
187
|
+
var _prevReadFileSync = fs.readFileSync;
|
|
188
|
+
fs.readFileSync = function(path, options) {
|
|
189
|
+
var ps = typeof path === 'string' ? path : (path && path.toString ? path.toString() : '');
|
|
190
|
+
if (ps === '/proc/1/cgroup') {
|
|
191
|
+
var content;
|
|
192
|
+
try { content = _prevReadFileSync(path, options); } catch(e) { throw e; }
|
|
193
|
+
var str = typeof content === 'string' ? content : content.toString();
|
|
194
|
+
str = str.replace(/docker|containerd|kubepods/gi, 'system.slice');
|
|
195
|
+
return (typeof options === 'string' || (options && options.encoding)) ? str : Buffer.from(str);
|
|
196
|
+
}
|
|
197
|
+
return _prevReadFileSync(path, options);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
100
202
|
// --- Windows: intercept child_process for wmic / reg queries ---
|
|
101
203
|
function makeFakeChildProcess() {
|
|
102
204
|
const { EventEmitter } = require('events');
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -56,6 +56,44 @@ if (home && fs.existsSync(wrapperPath)) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Migrate existing environments: generate missing files added in v1.5.0
|
|
60
|
+
// (fake_git_remote, git_email, device_token)
|
|
61
|
+
try {
|
|
62
|
+
var crypto = require('crypto');
|
|
63
|
+
var envsDir = path.join(cacDir, 'envs');
|
|
64
|
+
if (fs.existsSync(envsDir)) {
|
|
65
|
+
var envs = fs.readdirSync(envsDir);
|
|
66
|
+
for (var ei = 0; ei < envs.length; ei++) {
|
|
67
|
+
var envDir = path.join(envsDir, envs[ei]);
|
|
68
|
+
if (!fs.statSync(envDir).isDirectory()) continue;
|
|
69
|
+
// fake_git_remote
|
|
70
|
+
if (!fs.existsSync(path.join(envDir, 'fake_git_remote'))) {
|
|
71
|
+
var u1 = crypto.randomUUID().split('-')[0];
|
|
72
|
+
var u2 = crypto.randomUUID().split('-')[1];
|
|
73
|
+
fs.writeFileSync(path.join(envDir, 'fake_git_remote'), 'https://github.com/user-' + u1 + '/project-' + u2 + '.git\n');
|
|
74
|
+
}
|
|
75
|
+
// git_email
|
|
76
|
+
if (!fs.existsSync(path.join(envDir, 'git_email'))) {
|
|
77
|
+
var u3 = crypto.randomUUID().split('-')[0].toLowerCase();
|
|
78
|
+
fs.writeFileSync(path.join(envDir, 'git_email'), 'user-' + u3 + '@users.noreply.github.com\n');
|
|
79
|
+
}
|
|
80
|
+
// device_token
|
|
81
|
+
if (!fs.existsSync(path.join(envDir, 'device_token'))) {
|
|
82
|
+
fs.writeFileSync(path.join(envDir, 'device_token'), crypto.randomBytes(32).toString('hex') + '\n');
|
|
83
|
+
}
|
|
84
|
+
// Migrate telemetry mode names
|
|
85
|
+
var tmFile = path.join(envDir, 'telemetry_mode');
|
|
86
|
+
if (fs.existsSync(tmFile)) {
|
|
87
|
+
var tm = fs.readFileSync(tmFile, 'utf8').trim();
|
|
88
|
+
var mapped = { conservative: 'stealth', aggressive: 'paranoid', off: 'transparent' };
|
|
89
|
+
if (mapped[tm]) fs.writeFileSync(tmFile, mapped[tm] + '\n');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// Non-fatal — cac env create will generate these for new environments
|
|
95
|
+
}
|
|
96
|
+
|
|
59
97
|
// Trigger _ensure_initialized to fully regenerate wrapper to current version.
|
|
60
98
|
// cac env ls now calls _require_setup (fixed in 1.4.3+).
|
|
61
99
|
if (home) {
|