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 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.4.4"
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
- _telemetry_mode="conservative"
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
- # off: no telemetry intervention at all
1180
- # conservative: disable non-essential traffic only (looks like corporate bandwidth saving)
1181
- # aggressive: full 12-layer telemetry kill (maximum privacy, potentially detectable)
1182
- if [[ "$_telemetry_mode" != "off" ]]; then
1183
- export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
1184
- export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=
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" != "off" ]] && [[ "$_telemetry_mode" != "conservative" ]] && [[ "$_telemetry_mode" != "aggressive" ]]; then
1188
- echo "[cac] warning: unknown telemetry mode '$_telemetry_mode', using conservative" >&2
1189
- _telemetry_mode="conservative"
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" == "aggressive" ]]; then
1193
- # Layer 1: Claude Code native toggle
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
- [[ "$telemetry_mode" =~ ^(off|conservative|aggressive)$ ]] || _die "invalid telemetry mode '$telemetry_mode' (use off, conservative, or aggressive)" ;;
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: conservative (default) or aggressive
1642
- [[ -z "$telemetry_mode" ]] && telemetry_mode=$(_cac_setting telemetry_mode conservative)
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> Set proxy"
1846
- echo " $(_green "set") [name] proxy --remove Remove proxy"
1847
- echo " $(_green "set") [name] version <ver|latest> Change Claude version"
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 version"
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" "conservative")
2214
- local _tel_conservative_vars=("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC" "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA")
2215
- local _tel_aggressive_vars=(
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" == "off" ]]; then
2223
- echo " $(_dim "○") telemetry off (no protection)"
2224
- elif [[ "$telemetry_mode" == "aggressive" ]]; then
2225
- local env_ok=0 env_total=${#_tel_aggressive_vars[@]}
2226
- for var in "${_tel_aggressive_vars[@]}"; do
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 aggressive ${env_ok}/${env_total} blocked"
2336
+ echo " $(_green "✓") telemetry paranoid ${env_ok}/${env_total} blocked"
2231
2337
  else
2232
- echo " $(_red "✗") telemetry aggressive ${env_ok}/${env_total} blocked"
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 cons_ok=0
2237
- for var in "${_tel_conservative_vars[@]}"; do
2238
- [[ "$wrapper_content" == *"$var"* ]] && (( cons_ok++ )) || true
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 [[ "$cons_ok" -eq 2 ]]; then
2241
- echo " $(_green "✓") telemetry conservative (non-essential blocked)"
2346
+ if [[ "$stealth_ok" -eq 2 ]]; then
2347
+ echo " $(_green "✓") telemetry stealth (1p blocked, features normal)"
2242
2348
  else
2243
- echo " $(_red "✗") telemetry conservative ($cons_ok/2)"
2349
+ echo " $(_red "✗") telemetry stealth ($stealth_ok/2)"
2244
2350
  problems+=("telemetry shield incomplete")
2245
2351
  fi
2246
2352
  fi
2247
2353
 
2248
- # ── fingerprint hook runtime verification ──
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
- echo " $(_green "✓") fingerprint spoofed ($(_dim "$expected_hn"))"
2367
+ _fp_ok=true; (( _id_ok++ )) || true
2256
2368
  else
2257
- echo " $(_red "✗") fingerprint NOT spoofed (got: $actual_hn)"
2258
- problems+=("fingerprint hook not working")
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 global address detected (potential leak)"
2433
+ echo " $(_yellow "⚠") IPv6 global address detected (potential leak)"
2276
2434
  else
2277
- echo " $(_green "✓") IPv6 no global address"
2435
+ echo " $(_green "✓") IPv6 no global address"
2278
2436
  fi
2279
2437
 
2280
- # ── residual telemetry files ──
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 $tel_files telemetry files in ~/.claude/telemetry/"
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 $_claude_count running (threshold: $_max_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 "user_id") $(_read "$env_dir/user_id" "—")"
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" == "off" ]]; then
2572
+ if [[ "$telemetry_mode" == "transparent" ]]; then
2405
2573
  echo " $(_dim " no telemetry protection active")"
2406
2574
  fi
2407
- local _vvars=("${_tel_conservative_vars[@]}")
2408
- [[ "$telemetry_mode" == "aggressive" ]] && _vvars=("${_tel_aggressive_vars[@]}")
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
@@ -19,6 +19,7 @@ var BLOCKED_DOMAINS = new Set([
19
19
  'sentry.io',
20
20
  'o1137031.ingest.sentry.io',
21
21
  'cdn.growthbook.io',
22
+ 'http-intake.logs.us5.datadoghq.com',
22
23
  ]);
23
24
 
24
25
  /**
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-cac",
3
- "version": "1.4.4",
3
+ "version": "1.5.0-beta.2",
4
4
  "description": "Isolate, protect, and manage your Claude Code — versions, environments, identity, and proxy.",
5
5
  "bin": {
6
6
  "cac": "cac"
@@ -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) {