claude-cac 1.5.0-beta.1 → 1.5.0

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.
Files changed (3) hide show
  1. package/cac +84 -82
  2. package/fingerprint-hook.js +16 -31
  3. package/package.json +1 -1
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.5.0-beta.1"
14
+ CAC_VERSION="1.5.0"
15
15
 
16
16
  _read() { [[ -f "$1" ]] && tr -d '[:space:]' < "$1" || echo "${2:-}"; }
17
17
  _die() { printf '%b\n' "$(_red "error:") $*" >&2; exit 1; }
@@ -63,7 +63,7 @@ _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
64
  _new_git_remote() { echo "https://github.com/user-$(_gen_uuid | cut -d- -f1)/project-$(_gen_uuid | cut -d- -f2).git"; }
65
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"; }
66
+ _new_device_token() { _new_user_id; }
67
67
 
68
68
  # Get real command path (bypass shim)
69
69
  _get_real_cmd() {
@@ -1232,12 +1232,10 @@ if [[ -n "$PROXY" ]]; then
1232
1232
  fi
1233
1233
 
1234
1234
  # ── git identity spoofing ──
1235
- # Prevent git email leakage (Claude runs `git config --get user.email` on startup)
1235
+ # Intercept `git config --get user.email` at process level (telemetry read only)
1236
+ # Do NOT set GIT_AUTHOR_EMAIL/GIT_COMMITTER_EMAIL — those would affect real git commits
1236
1237
  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"
1238
+ export CAC_GIT_EMAIL=$(tr -d '[:space:]' < "$_env_dir/git_email")
1241
1239
  fi
1242
1240
 
1243
1241
  # ── repository fingerprint (rh) spoofing ──
@@ -1255,31 +1253,29 @@ fi
1255
1253
  # ── persona (Docker/server environment spoofing) ──
1256
1254
  if [[ -f "$_env_dir/persona" ]]; then
1257
1255
  _persona=$(tr -d '[:space:]' < "$_env_dir/persona")
1256
+ export TERM="xterm-256color"
1258
1257
  case "$_persona" in
1259
1258
  macos-vscode)
1260
1259
  export TERM_PROGRAM="vscode"
1261
1260
  export VSCODE_GIT_ASKPASS_MAIN="/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass-main.js"
1262
1261
  export __CFBundleIdentifier="com.microsoft.VSCode"
1263
- export TERM="xterm-256color"
1264
1262
  ;;
1265
1263
  macos-cursor)
1266
1264
  export TERM_PROGRAM="vscode"
1267
- export CURSOR_TRACE_ID="cursor-$(head -c 8 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n')"
1265
+ [[ -f "$_env_dir/cursor_trace_id" ]] || printf 'cursor-%s' "$(od -An -tx1 -N8 /dev/urandom | tr -d ' \n')" > "$_env_dir/cursor_trace_id"
1266
+ export CURSOR_TRACE_ID=$(tr -d '[:space:]' < "$_env_dir/cursor_trace_id")
1268
1267
  export __CFBundleIdentifier="com.todesktop.230313mzl4w4u92"
1269
- export TERM="xterm-256color"
1270
1268
  ;;
1271
1269
  macos-iterm)
1272
1270
  export TERM_PROGRAM="iTerm.app"
1273
1271
  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')"
1272
+ [[ -f "$_env_dir/iterm_session_id" ]] || printf 'w0t0p0:%s' "$(od -An -tx1 -N16 /dev/urandom | tr -d ' \n')" > "$_env_dir/iterm_session_id"
1273
+ export ITERM_SESSION_ID=$(tr -d '[:space:]' < "$_env_dir/iterm_session_id")
1276
1274
  ;;
1277
1275
  linux-desktop)
1278
1276
  export TERM_PROGRAM="vscode"
1279
- export TERM="xterm-256color"
1280
1277
  ;;
1281
1278
  esac
1282
- # Hide Docker signals when persona is active
1283
1279
  export CAC_HIDE_DOCKER=1
1284
1280
  fi
1285
1281
 
@@ -2351,22 +2347,74 @@ cmd_check() {
2351
2347
  fi
2352
2348
  fi
2353
2349
 
2354
- # ── fingerprint hook runtime verification ──
2350
+ # ── identity spoofing (consolidated) ──
2351
+ local os; os=$(_detect_os)
2352
+ local _id_ok=0 _id_total=0 _id_issues=()
2353
+
2354
+ # fingerprint hook
2355
+ local _fp_ok=false
2355
2356
  if [[ -f "$CAC_DIR/fingerprint-hook.js" ]] && [[ -f "$env_dir/hostname" ]]; then
2356
2357
  local expected_hn; expected_hn=$(_read "$env_dir/hostname")
2357
2358
  local actual_hn
2358
2359
  actual_hn=$(NODE_OPTIONS="--require $CAC_DIR/fingerprint-hook.js" CAC_HOSTNAME="$expected_hn" \
2359
2360
  node -e "process.stdout.write(require('os').hostname())" 2>/dev/null || true)
2361
+ (( _id_total++ )) || true
2360
2362
  if [[ "$actual_hn" == "$expected_hn" ]]; then
2361
- echo " $(_green "✓") fingerprint spoofed ($(_dim "$expected_hn"))"
2363
+ _fp_ok=true; (( _id_ok++ )) || true
2362
2364
  else
2363
- echo " $(_red "✗") fingerprint NOT spoofed (got: $actual_hn)"
2364
- problems+=("fingerprint hook not working")
2365
+ _id_issues+=("fingerprint hook not working")
2366
+ fi
2367
+ fi
2368
+ # git email
2369
+ (( _id_total++ )) || true
2370
+ if [[ -f "$env_dir/git_email" ]]; then
2371
+ (( _id_ok++ )) || true
2372
+ else
2373
+ _id_issues+=("git email not spoofed")
2374
+ fi
2375
+ # repo hash
2376
+ (( _id_total++ )) || true
2377
+ if [[ -f "$env_dir/fake_git_remote" ]]; then
2378
+ (( _id_ok++ )) || true
2379
+ else
2380
+ _id_issues+=("repo hash not spoofed")
2381
+ fi
2382
+ # user_id consistency
2383
+ local _uid_ok=true
2384
+ local _env_uid; _env_uid=$(_read "$env_dir/user_id" "")
2385
+ if [[ -n "$_env_uid" ]]; then
2386
+ local _config_dir="${CLAUDE_CONFIG_DIR:-$ENVS_DIR/$current/.claude}"
2387
+ local _cj="$_config_dir/.claude.json"
2388
+ [[ -f "$_cj" ]] || _cj="$HOME/.claude.json"
2389
+ if [[ -f "$_cj" ]]; then
2390
+ local _actual_uid
2391
+ _actual_uid=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('userID',''))" "$_cj" 2>/dev/null || true)
2392
+ (( _id_total++ )) || true
2393
+ if [[ -n "$_actual_uid" ]] && [[ "$_actual_uid" != "$_env_uid" ]]; then
2394
+ _uid_ok=false
2395
+ _id_issues+=("user_id mismatch")
2396
+ else
2397
+ (( _id_ok++ )) || true
2398
+ fi
2365
2399
  fi
2366
2400
  fi
2401
+ # billing header
2402
+ (( _id_total++ )) || true
2403
+ [[ "$wrapper_content" == *"CLAUDE_CODE_ATTRIBUTION_HEADER"* ]] && { (( _id_ok++ )) || true; } || _id_issues+=("billing header exposed")
2404
+
2405
+ # display consolidated identity line
2406
+ local _id_extra=""
2407
+ [[ -f "$env_dir/persona" ]] && _id_extra=" + $(_read "$env_dir/persona")"
2408
+ if [[ "$_id_ok" -eq "$_id_total" ]]; then
2409
+ echo " $(_green "✓") identity ${_id_ok}/${_id_total} spoofed${_id_extra}"
2410
+ else
2411
+ echo " $(_red "✗") identity ${_id_ok}/${_id_total} spoofed${_id_extra}"
2412
+ for _ii in "${_id_issues[@]}"; do
2413
+ problems+=("$_ii")
2414
+ done
2415
+ fi
2367
2416
 
2368
2417
  # ── IPv6 leak detection ──
2369
- local os; os=$(_detect_os)
2370
2418
  local ipv6_leak=false
2371
2419
  if [[ "$os" == "macos" ]]; then
2372
2420
  local ipv6_addrs
@@ -2378,80 +2426,28 @@ cmd_check() {
2378
2426
  [[ "$ipv6_addrs" -gt 0 ]] && ipv6_leak=true
2379
2427
  fi
2380
2428
  if [[ "$ipv6_leak" == "true" ]]; then
2381
- echo " $(_yellow "⚠") IPv6 global address detected (potential leak)"
2429
+ echo " $(_yellow "⚠") IPv6 global address detected (potential leak)"
2382
2430
  else
2383
- echo " $(_green "✓") IPv6 no global address"
2431
+ echo " $(_green "✓") IPv6 no global address"
2384
2432
  fi
2385
2433
 
2386
- # ── residual telemetry files ──
2434
+ # ── warnings (only shown when relevant) ──
2387
2435
  if [[ -d "$HOME/.claude/telemetry" ]]; then
2388
2436
  local tel_files
2389
2437
  tel_files=$(find "$HOME/.claude/telemetry" -type f 2>/dev/null | wc -l | tr -d ' ')
2390
2438
  if [[ "$tel_files" -gt 0 ]]; then
2391
- echo " $(_yellow "⚠") residual $tel_files telemetry files in ~/.claude/telemetry/"
2392
- echo " $(_dim "hint: rm -rf ~/.claude/telemetry/")"
2439
+ echo " $(_yellow "⚠") residual $tel_files telemetry files in ~/.claude/telemetry/"
2393
2440
  fi
2394
2441
  fi
2395
-
2396
- # ── concurrent sessions ──
2397
2442
  local _claude_count
2398
2443
  _claude_count=$(pgrep -x "claude" 2>/dev/null | wc -l | tr -d '[:space:]') || _claude_count=0
2399
2444
  local _max_sessions; _max_sessions=$(_cac_setting max_sessions 10)
2400
2445
  if [[ "$_claude_count" -gt "$_max_sessions" ]]; then
2401
- echo " $(_yellow "⚠") sessions $_claude_count running (threshold: $_max_sessions)"
2402
- fi
2403
-
2404
- # ── metadata.user_id consistency ──
2405
- local _env_uid; _env_uid=$(_read "$env_dir/user_id" "")
2406
- if [[ -n "$_env_uid" ]]; then
2407
- local _config_dir="${CLAUDE_CONFIG_DIR:-$ENVS_DIR/$current/.claude}"
2408
- local _cj="$_config_dir/.claude.json"
2409
- [[ -f "$_cj" ]] || _cj="$HOME/.claude.json"
2410
- if [[ -f "$_cj" ]]; then
2411
- local _actual_uid
2412
- _actual_uid=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('userID',''))" "$_cj" 2>/dev/null || true)
2413
- if [[ -n "$_actual_uid" ]] && [[ "$_actual_uid" != "$_env_uid" ]]; then
2414
- echo " $(_yellow "⚠") user_id mismatch — Claude may have overwritten it"
2415
- echo " $(_dim "env: ${_env_uid:0:16}... json: ${_actual_uid:0:16}...")"
2416
- problems+=("user_id mismatch between env file and .claude.json")
2417
- else
2418
- echo " $(_green "✓") user_id consistent"
2419
- fi
2420
- fi
2421
- fi
2422
-
2423
- # ── git email spoofing ──
2424
- if [[ -f "$env_dir/git_email" ]]; then
2425
- echo " $(_green "✓") git email spoofed ($(_dim "$(_read "$env_dir/git_email")"))"
2426
- else
2427
- echo " $(_yellow "⚠") git email not spoofed (real email exposed)"
2428
- fi
2429
-
2430
- # ── repository fingerprint (rh) ──
2431
- if [[ -f "$env_dir/fake_git_remote" ]]; then
2432
- echo " $(_green "✓") repo hash spoofed"
2433
- else
2434
- echo " $(_yellow "⚠") repo hash not spoofed (rh links activity across accounts)"
2446
+ echo " $(_yellow "⚠") sessions $_claude_count running (threshold: $_max_sessions)"
2435
2447
  fi
2436
-
2437
- # ── billing header ──
2438
- if [[ "$wrapper_content" == *"CLAUDE_CODE_ATTRIBUTION_HEADER"* ]]; then
2439
- echo " $(_green "✓") billing header disabled"
2440
- fi
2441
-
2442
- # ── Keychain residual (macOS) ──
2443
2448
  if [[ "$os" == "macos" ]]; then
2444
- local _kc_found=false
2445
- security find-generic-password -s "claude-code-credentials" >/dev/null 2>&1 && _kc_found=true
2446
- if [[ "$_kc_found" == "true" ]]; then
2447
- echo " $(_yellow "⚠") keychain Trusted Device Token found in macOS Keychain"
2448
- echo " $(_dim "hint: security delete-generic-password -s 'claude-code-credentials'")"
2449
- fi
2450
- fi
2451
-
2452
- # ── persona ──
2453
- if [[ -f "$env_dir/persona" ]]; then
2454
- echo " $(_green "✓") persona $(_dim "$(_read "$env_dir/persona")")"
2449
+ security find-generic-password -s "claude-code-credentials" >/dev/null 2>&1 && \
2450
+ echo " $(_yellow "") keychain Trusted Device Token residual"
2455
2451
  fi
2456
2452
 
2457
2453
  # ── network check (slow — streaming output) ──
@@ -2551,13 +2547,19 @@ cmd_check() {
2551
2547
 
2552
2548
  # ── verbose mode ──
2553
2549
  if [[ "$verbose" == "true" ]]; then
2550
+ echo " $(_bold "Identity")"
2551
+ echo " $([[ "$_fp_ok" == "true" ]] && _green "✓" || _red "✗") hostname $(_read "$env_dir/hostname" "—")"
2552
+ echo " $([[ -f "$env_dir/git_email" ]] && _green "✓" || _yellow "⚠") git email $(_read "$env_dir/git_email" "—")"
2553
+ echo " $([[ -f "$env_dir/fake_git_remote" ]] && _green "✓" || _yellow "⚠") repo hash $(_read "$env_dir/fake_git_remote" "—")"
2554
+ echo " $([[ "$_uid_ok" == "true" ]] && _green "✓" || _yellow "⚠") user_id $(_read "$env_dir/user_id" "—" | cut -c1-16)..."
2555
+ echo " $([[ "$wrapper_content" == *"CLAUDE_CODE_ATTRIBUTION_HEADER"* ]] && _green "✓" || _yellow "⚠") billing $([[ "$wrapper_content" == *"CLAUDE_CODE_ATTRIBUTION_HEADER"* ]] && echo "disabled" || echo "exposed")"
2556
+ [[ -f "$env_dir/persona" ]] && echo " $(_green "✓") persona $(_read "$env_dir/persona")"
2557
+ echo
2554
2558
  echo " $(_bold "Details")"
2555
2559
  echo " $(_dim "UUID") $(_read "$env_dir/uuid")"
2556
2560
  echo " $(_dim "stable_id") $(_read "$env_dir/stable_id")"
2557
- echo " $(_dim "user_id") $(_read "$env_dir/user_id" "—")"
2558
- echo " $(_dim "git_email") $(_read "$env_dir/git_email" "—")"
2559
- echo " $(_dim "rh_remote") $(_read "$env_dir/fake_git_remote" "—")"
2560
- echo " $(_dim "persona") $(_read "$env_dir/persona" "—")"
2561
+ echo " $(_dim "MAC") $(_read "$env_dir/mac_address" "—")"
2562
+ echo " $(_dim "machine_id") $(_read "$env_dir/machine_id" "—")"
2561
2563
  echo " $(_dim "TZ") $(_read "$env_dir/tz" "—")"
2562
2564
  echo " $(_dim "LANG") $(_read "$env_dir/lang" "—")"
2563
2565
  echo " $(_dim "env") ${env_dir/#$HOME/~}/.claude/"
@@ -115,11 +115,7 @@ if (fakeGitRemote) {
115
115
  const _origExecSyncFp = child_process.execSync.bind(child_process);
116
116
  child_process.execSync = function(cmd, options) {
117
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
- }
118
+ if (isGitRemoteCmd(cmdStr)) return fakeResult(options, fakeGitRemote + '\n');
123
119
  return _origExecSyncFp(cmd, options);
124
120
  };
125
121
 
@@ -130,11 +126,7 @@ if (fakeGitRemote) {
130
126
  var cb = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : null;
131
127
  if (isGitRemoteCmd(cmdStr)) {
132
128
  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;
129
+ return makeFakeChildProcess();
138
130
  }
139
131
  return _origExecFp.apply(child_process, args);
140
132
  };
@@ -145,9 +137,7 @@ if (fakeGitRemote) {
145
137
  var fullCmd = file + ' ' + fileArgs.join(' ');
146
138
  if (isGitRemoteCmd(fullCmd)) {
147
139
  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);
140
+ return fakeResult(opts, fakeGitRemote + '\n');
151
141
  }
152
142
  return _origExecFileSyncFp(file, argsOrOpts, options);
153
143
  };
@@ -158,14 +148,11 @@ if (fakeGitRemote) {
158
148
  // Intercept to prevent real email leakage (wrapper also sets GIT_AUTHOR_EMAIL)
159
149
  const fakeGitEmail = process.env.CAC_GIT_EMAIL;
160
150
  if (fakeGitEmail) {
161
- // Re-wrap execSync if not already wrapped for git remote
162
151
  var _prevExecSync = child_process.execSync;
163
152
  child_process.execSync = function(cmd, options) {
164
153
  var cmdStr = typeof cmd === 'string' ? cmd : cmd.toString();
165
154
  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);
155
+ return fakeResult(options, fakeGitEmail + '\n');
169
156
  }
170
157
  return _prevExecSync(cmd, options);
171
158
  };
@@ -183,20 +170,18 @@ if (process.env.CAC_HIDE_DOCKER === '1') {
183
170
  };
184
171
 
185
172
  // 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
- }
173
+ var _prevReadFileSync = fs.readFileSync;
174
+ fs.readFileSync = function(path, options) {
175
+ var ps = typeof path === 'string' ? path : (path && path.toString ? path.toString() : '');
176
+ if (ps === '/proc/1/cgroup') {
177
+ var content;
178
+ try { content = _prevReadFileSync(path, options); } catch(e) { throw e; }
179
+ var str = typeof content === 'string' ? content : content.toString();
180
+ str = str.replace(/docker|containerd|kubepods/gi, 'system.slice');
181
+ return fakeResult(options, str);
182
+ }
183
+ return _prevReadFileSync(path, options);
184
+ };
200
185
  }
201
186
 
202
187
  // --- Windows: intercept child_process for wmic / reg queries ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-cac",
3
- "version": "1.5.0-beta.1",
3
+ "version": "1.5.0",
4
4
  "description": "Isolate, protect, and manage your Claude Code — versions, environments, identity, and proxy.",
5
5
  "bin": {
6
6
  "cac": "cac"