Skip to content

Instantly share code, notes, and snippets.

@ahundt
Last active March 17, 2026 20:59
Show Gist options
  • Select an option

  • Save ahundt/158d498f07a10fec4955a70bf475a20b to your computer and use it in GitHub Desktop.

Select an option

Save ahundt/158d498f07a10fec4955a70bf475a20b to your computer and use it in GitHub Desktop.
Fix: DISABLE_TELEMETRY blocks 1M context and /remote-control in Claude Code v2.1.76

Fix: DISABLE_TELEMETRY Blocks 1M Context and /remote-control in Claude Code

Binary patch for Claude Code that fixes the bug where setting DISABLE_TELEMETRY=1 silently disables ALL feature gates — blocking 1M context, /remote-control, and every other gated feature — not just telemetry. Tested on v2.1.76 and v2.1.77, Max 20x, macOS. Works on macOS, Linux, and Windows.

For a simpler workaround that only fixes 1M context (no patching required), see Simple Workaround below.

After patching, gated features work while all data-collection env vars (DISABLE_TELEMETRY, DISABLE_ERROR_REPORTING, DISABLE_AUTOUPDATER, etc.) continue to work exactly as before.

Related issues: #29580 — source code trace proving the bug | #34083 — 1M context + /remote-control blocked on Max plan | #34143 — community-confirmed root cause analysis

Step-by-Step Install (Full Binary Patch)

Prerequisites: Python 3, Git.

Step 1. Clone this gist

git clone https://gist.github.com/ahundt/158d498f07a10fec4955a70bf475a20b \
  ~/.claude/claude-patch-fix-disable-telemetry-blocks-1m-context-remote-control

Step 2. Enter the directory

cd ~/.claude/claude-patch-fix-disable-telemetry-blocks-1m-context-remote-control

Step 3. Dry-run first (changes nothing, shows what would be patched)

python3 patch_claude_binary.py

Step 4. Apply the patches for real

python3 patch_claude_binary.py --real-run

A backup is created automatically next to the binary (e.g., ~/.local/share/claude/versions/2.1.77.bak.20260317_160355).

Step 5. Verify — open a NEW Claude Code session and check:

  • /context → should show 1000k max (not 200k)
  • /model opus[1m] → should succeed (not "not available for your account")
  • /help → should list /remote-control

Step 6. (Optional) Rollback if needed

python3 patch_claude_binary.py --rollback --real-run

Use --rollback-list to see all backups.

Simple Workaround: 1M Context Only (No Patching)

If you only need 1M context (not /remote-control or other gated features), you can bypass the client-side check with an env var — no binary patching needed.

One-time launch:

ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-6[1m] claude

Make it permanent — add to ~/.zshrc (or ~/.bashrc):

export ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-6[1m]

/model opus[1m] is rejected client-side (s1mAccessCache), but the server accepts it on Max 20x after the March 13 GA. Env vars are passed to the provider as-is, skipping that check. Banner confirms Opus 4.6 (1M context).

Credit: @ln-north, who tested on v2.1.76, Max 20x, macOS. Not independently verified by this repo's author.

After a Claude Code Auto-Update

When Claude Code updates itself, the new binary will be unpatched. Re-patch:

cd ~/.claude/claude-patch-fix-disable-telemetry-blocks-1m-context-remote-control
python3 patch_claude_binary.py --real-run

For unknown versions the script auto-discovers function names using stable structural patterns. If auto-discovery also fails (e.g., code structure changed), it aborts with a clear error. See Adapting to Future Versions below.

Files in This Directory

File Purpose
patch_claude_binary.py Apply/rollback patches. Idempotent, dry-run default, auto-backup. Supports v2.1.76 and v2.1.77 via KNOWN_VERSIONS; auto-discovers function names for newer versions. Contains an investigation methodology in the docstring for manually tracing patches in any version.
analyze_claude_binary.py Investigate the binary. 15 subcommands for version detection, function extraction, call chain tracing, patch status, traffic analysis, GrowthBook/Segment SDK deep-dives, and arbitrary pattern search. Run python3 analyze_claude_binary.py all for a full dump.
binary_patch_...bug.md Original patch design document with before/after code, byte offsets, JS precedence analysis, and safety design.
README.md This file — quick start, patch summary, and the full investigation report below.

What the Patches Do

Two patches are applied. All other functions are left intact so that data-collection env vars continue to work normally. Minified function names change each release; the table shows the role name and the per-version identifiers.

Role v2.1.76 name v2.1.77 name Patch Effect
statsig_gate Ai() hi() return analytics_gate()return!1||!0 Statsig/GrowthBook gate lookups always proceed; decouples feature gates from analytics state
extra_usage_check bf7() Ij7() return!1return!0 for undefined branch Fixes 1M race condition: undefined means "not yet loaded", not "disabled"
telemetry_gate Zv() Cv() not patched DISABLE_TELEMETRY=1 continues to disable analytics events and block Segment
analytics_gate u1_() o1_() not patched Analytics follow env var: off when DISABLE_TELEMETRY=1, on otherwise
Segment init b27() varies not patched Segment follows env var: blocked when DISABLE_TELEMETRY=1, works otherwise
feedback_gate L8T() v8T() not patched Feedback surveys stay suppressed when DISABLE_TELEMETRY=1

Data-collection flag status (all env-controlled after patching):

Flag Controls After patching
DISABLE_TELEMETRY=1 Analytics events, Segment SDK Off/blocked (env-controlled, same as before patching)
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 Same as above (umbrella) Off/blocked (env-controlled)
DISABLE_ERROR_REPORTING=1 Sentry error logging Off (env-controlled, patches don't touch this)
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY=1 Feedback survey popup Off (env-controlled, patches don't touch this)
DISABLE_AUTOUPDATER=1 Auto-updater Off (env-controlled, patches don't touch this)
DISABLE_COST_WARNINGS=1 Cost warnings Off (env-controlled, patches don't touch this)

Adapting to Future Versions

When Claude Code updates, minified function names change (e.g., ZvCv, u1_o1_). The script handles this in two ways:

Automatic (try first): For unknown versions, discover_function_names() searches for stable structural patterns (unminified string literals that never change) and discovers all function names automatically. If it succeeds, no manual work is needed.

Manual (if auto-discovery fails): This means the code structure changed, not just the names. To find the new functions:

  1. Add the new version's names to KNOWN_VERSIONS in patch_claude_binary.py and re-run. The KNOWN_VERSIONS dict maps version strings to role→name dicts.
  2. To find those names, use analyze_claude_binary.py search and context subcommands with stable strings that don't get minified:
    • !!process.env.DISABLE_TELEMETRY — finds the telemetry gate function
    • s_(process.env.CLAUDE_CODE_USE_FOUNDRY) — finds the context anchor
    • cachedExtraUsageDisabledReason — finds the extra-usage race condition function
    • LKJN8LsLERHEOXkw487o7qCTFOrGPimI — finds Segment SDK init
    • tengu_log_segment_events — finds event gate names
    • cachedGrowthBookFeatures — finds local cache writer
    • remoteEval:!0 — finds GrowthBook instance creation
  3. Read the Investigation Methodology in patch_claude_binary.py (docstring) for a step-by-step process to trace the full call chain in any version.

Known Limitations (inherent — cannot be patched away)

Feature gate evaluation contacts Anthropic servers

The Ai() patch enables Statsig/GrowthBook feature gate evaluation. This requires network contact with Anthropic's servers even when DISABLE_TELEMETRY=1:

  • GrowthBook remoteEval — sends user metadata to api.anthropic.com: deviceId, sessionId, platform, orgUUID, accountUUID, userType, subscriptionType, rateLimitTier. No conversation content. Goes to the same server your conversations go to.
  • Statsig gate exposure events — Statsig's SDK logs which feature gates were evaluated for your session. These are read-only flag lookups, not usage analytics, but they are transmitted to Anthropic's Statsig instance.

This contact cannot be eliminated without also disabling feature gates entirely (which means losing 1M context, /remote-control, etc.). It is the irreducible minimum required for gated features to work.

DISABLE_TELEMETRY may be linked to training data permission in Anthropic's ToS

Anthropic's data usage policy links telemetry opt-out to training data permissions. Enabling feature gate evaluation (by applying the Ai() patch) may constitute "allowing telemetry" in the policy sense, which could be interpreted as granting permission for data to be used for model training.

What these patches protect:

  • Client-side analytics events (Statsig usage tracking, Segment) — blocked when DISABLE_TELEMETRY=1, via intact u1_() and b27() code paths ✓
  • Sentry error reporting — blocked when DISABLE_ERROR_REPORTING=1
  • Feedback surveys — suppressed when DISABLE_TELEMETRY=1 via intact L8T()

What these patches do NOT protect:

  • Feature gate evaluation network contact (GrowthBook remoteEval, Statsig gate exposure) — enabled by the Ai() patch. This was blocked before patching.
  • Server-side data retention and training policies — governed by Anthropic's ToS, not by client binary code. These patches have no effect on what Anthropic does with data that reaches their servers.

Recommendation: Review Anthropic's current data usage policy and privacy settings in your account before applying. If the training data concern outweighs the value of 1M context / /remote-control, do not apply the Ai() patch — or roll it back with python3 patch_claude_binary.py --rollback --real-run.


Patch Design Notes — Minimal vs Over-Engineered Approaches

Why Minimal Patching Matters

The goal is to fix the specific structural bug (feature gates coupled to analytics state) while leaving all other code paths — especially env-var-driven data collection controls — completely intact. Every function left unpatched is one more thing that correctly respects user configuration.

Rule of thumb: find the smallest cut in the dependency graph that separates the two concerns. Do not patch downstream effects when you can cut the root coupling.

The Over-Engineered Approach (5 patches — do not use)

An earlier version of this patch applied 5 functions: Zv() (telemetry_gate), u1_() (analytics_gate), Ai() (statsig_gate), b27() (Segment init), and bf7() (extra_usage_check) — using v2.1.76 names. The intent was correct but the scope was too wide.

What went wrong:

The call chain when DISABLE_TELEMETRY=1 is (using v2.1.76 names; roles in brackets):

Zv() = true          [telemetry_gate] ← reads DISABLE_TELEMETRY
  → u1_() = !Zv() = false   [analytics_gate]  ← analytics off ✓
  → BKK() = !Zv() = false   → b27 returns null [Segment init] ← Segment blocked ✓
  → Ai() = u1_() = false    [statsig_gate] → Oq() bails ← ALL feature gates: default false ✗ BUG

The bug is only in the last link: Ai() calling u1_(). To fix it, only that link needs cutting.

The over-engineered approach instead patched Zv() to always return 0 (ignoring DISABLE_TELEMETRY), then had to also hardcode u1_()=false (otherwise analytics re-enabled), and hardcode b27=(0&&0) (otherwise Segment re-initialized). This created three hardcoded outcomes that no longer respected the env vars. A user who later removed DISABLE_TELEMETRY from their settings would still have analytics off and Segment blocked — the binary would ignore their changed preference.

The Minimal Approach (2 patches — what this script applies)

Using v2.1.76 names; roles apply to all versions:

patch: Ai() [statsig_gate] = true   ← always (decouples gates from analytics)
patch: bf7() [extra_usage_check] undefined → true  ← fixes 1M race condition

Zv() [telemetry_gate], u1_() [analytics_gate], b27() [Segment init], L8T() [feedback_gate]
— all intact, all env-var driven

With DISABLE_TELEMETRY=1: Zv()=trueu1_()=false (analytics off), BKK()=false (Segment blocked), Ai()=true (gates work). All flags respected.

Without DISABLE_TELEMETRY: Zv()=falseu1_()=true (analytics on), BKK()=true (Segment works), Ai()=true (gates work). Removing the flag fully restores original behavior.

How to Find the Minimal Patch in Future Versions

When a new Claude Code version changes minified names, follow this process:

Step 1 — Identify the symptom precisely. Which feature is blocked? Which env var triggers it? In this case: DISABLE_TELEMETRY=1 → feature gates return false defaults.

Step 2 — Find the feature gate evaluation function using stable strings. Feature gate queries always call a function that returns a boolean per gate name. Search for stable patterns that won't be minified:

python3 analyze_claude_binary.py context 'cachedGrowthBookFeatures' --window 400
python3 analyze_claude_binary.py context 'remoteEval:!0' --window 400

Look for the function that bails out early: if(!XYZ())return defaultValue. XYZ() is the new name of Ai().

Step 3 — Trace what XYZ() depends on.

python3 analyze_claude_binary.py context 'function XYZ()' --window 100

It will return some other function, e.g. return ABC(). That's the new u1_().

Step 4 — Trace what ABC() depends on.

python3 analyze_claude_binary.py context 'function ABC()' --window 100

It should return !DEF() where DEF() is the new Zv().

Step 5 — Confirm DEF() reads the env var.

python3 analyze_claude_binary.py context 'function DEF()' --window 300

You should see !!process.env.DISABLE_TELEMETRY inside it.

Step 6 — The minimal patch is always: patch XYZ() to return a constant true.

// Before:
function XYZ() { return ABC() }
// After (same byte length — adjust if names differ in length):
function XYZ() { return!1||!0 }

!1||!0 = false||true = true. Same byte count as return ABC() when ABC is a 3-character name. For 4-character names use return!0 anchored differently, or find a same-length constant expression. Never change byte count — this corrupts the binary.

Step 7 — Verify the analytics chain is intact (not accidentally patched). After patching XYZ(), confirm:

  • DEF() still contains !!process.env.DISABLE_TELEMETRY
  • ABC() still returns !DEF()
  • Analytics senders (search for Segment, analytics.segment.com) still check ABC() or DEF()

Step 8 — Look for the race condition pattern in any "extra usage" gate. The bf7() pattern — undefined treated as "disabled" instead of "not yet loaded" — may recur in future versions wherever async-loaded state is checked before the API responds. Search for:

python3 analyze_claude_binary.py context 'cachedExtraUsageDisabledReason' --window 200

If the function returns !1 (false) for void 0 (undefined), apply the same 1-byte fix: return!1return!0 for the undefined branch only.

Summary of the methodology:

  1. Find the gate evaluation bailout function (XYZ / statsig_gate equivalent)
  2. Confirm it depends on the analytics/telemetry state function
  3. Add the new names to KNOWN_VERSIONS in patch_claude_binary.py and re-run (or let discover_function_names() auto-discover them if structure is unchanged)
  4. Leave everything upstream (DEF/telemetry_gate, ABC/analytics_gate, Segment init) intact
  5. Verify upstream chain still respects env vars

Investigation Report: Claude Code 1M Context Window Bug

Date: 2026-03-15 Claude Code Version: 2.1.76 (latest) Plan: Claude Max Account Issues:

  • /model opus[1m] returns "Opus 4.6 with 1M context is not available for your account"
  • /remote-control is unavailable (same root cause)

Root Cause: DISABLE_TELEMETRY Blocks All Feature Flags Including 1M Context

The Bug

Setting DISABLE_TELEMETRY=1 or CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 in ~/.claude/settings.json disables all feature-gate evaluation, not just telemetry. This means Claude Code cannot check whether the account is entitled to 1M context, /remote-control, or any other gated feature — so it defaults to false for all of them.

Note: DISABLE_ERROR_REPORTING is a separate setting (Sentry error logging) and is not present in the Pv() function based on the traced source code. It may not trigger this bug on its own, but it is commonly set alongside DISABLE_TELEMETRY and could potentially be related through a different code path not yet traced. Most users who reported the fix removed both variables simultaneously, so independent confirmation is limited.

Source Code Evidence

User @Tuxerino420 traced the bug through the Claude Code source in issue #29580, comment.

Impacted file and binary:

The Claude Code CLI is distributed as a single compiled Bun executable. Platform-specific formats:

  • macOS: Mach-O 64-bit executable arm64 (or x86_64)
  • Linux: ELF 64-bit executable
  • Windows: PE32+ executable (claude.exe)

Default binary locations:

  • macOS/Linux: ~/.local/bin/claude (symlink → ~/.local/share/claude/versions/<version>)
  • Windows: %LOCALAPPDATA%\AnthropicClaude\app-<version>\claude.exe

The JavaScript source is bundled and minified inside this binary — there is no standalone cli.js file on disk. The npm package @anthropic-ai/claude-code exposes a single bin: claude entry point. All function names below (Pv, qA, Ar, Zvq, vn, w1) are minified identifiers from the embedded JS bundle and may change across versions.

@Tuxerino420's analysis was performed on Claude Code v2.1.69. The same code structure is present in v2.1.76 based on identical observed behavior.

Call chain (traced from command registration to feature gate):

Command registration: isEnabled: Zvq
  Zvq() → vn() → qA("tengu_ccr_bridge", false) → Ar() → !Pv()

The bug is in the Pv() function (determines whether to skip feature-flag evaluation):

function Pv() {
  return w1(process.env.CLAUDE_CODE_USE_BEDROCK) ||
         w1(process.env.CLAUDE_CODE_USE_VERTEX) ||
         w1(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
         !!process.env.DISABLE_TELEMETRY ||                          // <-- BUG: uses !! instead of w1()
         !!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC      // <-- BUG: uses !! instead of w1()
}

How it propagates:

  1. Pv() returns true when DISABLE_TELEMETRY is set to any non-empty string (including "0" or "false")
  2. Ar() calls !Pv(), which returns false
  3. qA() sees false and skips all GrowthBook/Statsig feature flag lookups, returning the default value (false) for every gated feature
  4. For /remote-control: the command registration uses isEnabled: Zvq — when Zvq returns false, the command is completely hidden from /help, not just disabled
  5. For 1M context: the entitlement check defaults to false, so Claude Code rejects /model opus[1m]

Features confirmed gated by this path:

  • 1M context window entitlement
  • /remote-control command availability (gated by tengu_ccr_bridge flag)
  • Any other feature-gated functionality

Cached feature flags: ~/.claude/.claude.json — @Tuxerino420 confirmed this file correctly contained "tengu_ccr_bridge": true for the account, but the value was never read because qA() bailed out early due to Pv() returning true.

The !! vs w1() sub-bug: DISABLE_TELEMETRY uses !! (JavaScript truthy check) while CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, and CLAUDE_CODE_USE_FOUNDRY all use w1() (proper boolean parser) in the same function. Setting DISABLE_TELEMETRY=0 in settings.json produces process.env.DISABLE_TELEMETRY = "0", and !!"0" is true in JavaScript because "0" is a non-empty string. So explicitly setting telemetry to OFF ("0") has the same effect as ON ("1").

Suggested Patch

From @Tuxerino420 in #29580:

Minimal fix — use the same w1() boolean parser already used for the other env vars in Pv():

// Before (broken):
!!process.env.DISABLE_TELEMETRY

// After (consistent with Bedrock/Vertex/Foundry checks):
w1(process.env.DISABLE_TELEMETRY)

Apply the same fix to CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:

// Before (broken):
!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC

// After (consistent):
w1(process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)

This would make "0" and "false" behave as expected (falsy), matching the behavior of CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, etc.

Better fix — decouple telemetry preferences from feature entitlements entirely. The Pv() function's purpose is to determine if the client should contact Statsig/GrowthBook, but it conflates "user doesn't want telemetry" with "user is on a third-party provider that can't reach Anthropic's feature-flag servers." These are fundamentally different situations. Telemetry opt-out should disable analytics data collection without affecting feature-gate evaluation for paid entitlements.

Source: All source code analysis, call chain trace, and patch suggestion from @Tuxerino420 in #29580 (Claude Code v2.1.69, Fedora Linux + Windows 11).

The Proper Fix (Not Yet Applied)

Use the same w1() boolean parser already used for Bedrock/Vertex/Foundry env vars:

// Before (broken):
!!process.env.DISABLE_TELEMETRY

// After (consistent):
w1(process.env.DISABLE_TELEMETRY)

More fundamentally: telemetry preferences should not gate functional features. These should be independent settings evaluated through separate code paths.

Workaround: Binary Patch (This Repository)

Update: A binary patch is now available in this repository (patch_claude_binary.py) that fixes the feature-gate bypass while keeping DISABLE_TELEMETRY=1 in settings. See the Quick Start at the top of this file.

Without the binary patch: There is no configuration-only workaround. The feature-gate lookup is completely bypassed — the telemetry setting and the entitlement check share the same code path. Every confirmed fix without patching involves removing DISABLE_TELEMETRY from settings. Note: adding CLAUDE_CODE_ENABLE_TELEMETRY=1 does NOT fix it — the DISABLE_TELEMETRY key must be removed entirely.

Why This Is a Bug, Not a Feature

  1. The official docs describe DISABLE_TELEMETRY as opting out of "analytics and usage tracking" — not opting out of paid features
  2. The GA announcement says 1M context is "included with subscription" for Max plans with "no additional configuration"
  3. The model config docs say "Opus is automatically upgraded to 1M context with no additional configuration" on Max plans
  4. Trail of Bits recommends disabling telemetry as a security best practice
  5. Users should not have to choose between privacy and access to paid features

Official Announcement

1M Context GA — March 13, 2026

Link: 1M context is now generally available for Opus 4.6 and Sonnet 4.6

Key claims:

  • Both Claude Opus 4.6 and Sonnet 4.6 now include the full 1M context window at standard pricing
  • No beta header required — requests over 200K tokens work automatically
  • Standard pricing across the full window ($5/$25 per million tokens for Opus 4.6, $3/$15 for Sonnet 4.6) — no multiplier
  • 1M context is included in Claude Code for Max, Team, and Enterprise users with Opus 4.6
  • Up to 600 images/PDF pages per request (up from 100)
  • Available on Claude Platform, Microsoft Azure Foundry, and Google Cloud Vertex AI

Official Documentation

Model Configuration Docs

Link: Model configuration - Claude Code Docs

On Max, Team, and Enterprise plans, Opus is automatically upgraded to 1M context with no additional configuration.

Plan Opus 4.6 with 1M context Sonnet 4.6 with 1M context
Max, Team, Enterprise Included with subscription Requires extra usage
Pro Requires extra usage Requires extra usage
API and pay-as-you-go Full access Full access
  • To explicitly disable 1M context: set CLAUDE_CODE_DISABLE_1M_CONTEXT=1
  • Use [1m] suffix with aliases: /model opus[1m], /model sonnet[1m]
  • "If you don't see it, try restarting your session"

Data Usage / Telemetry Docs

Link: Data usage - Claude Code Docs

  • DISABLE_TELEMETRY opts out of Statsig analytics/usage tracking
  • DISABLE_ERROR_REPORTING opts out of Sentry error logging
  • CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is an umbrella that disables all non-essential traffic (also disables auto-updates per Trail of Bits)

Common privacy-focused configuration (recommended by Trail of Bits and privacy guides) that triggers this bug:

{
  "env": {
    "DISABLE_TELEMETRY": "1",
    "DISABLE_ERROR_REPORTING": "1",
    "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1"
  }
}

Note: DISABLE_NON_ESSENTIAL_MODEL_CALLS does not exist in v2.1.76 (0 occurrences in the binary) — it has no effect and should be removed from configs that reference it.

Of these flags, DISABLE_TELEMETRY is confirmed to trigger the feature-gate bypass. CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY is not in the Pv() function and should be safe. DISABLE_ERROR_REPORTING is not confirmed in Pv() but may be related through an untraced code path.

The Trail of Bits config also recommends the equivalent umbrella setting CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, which is confirmed to trigger the same bug via the Pv() function.


Claude Code Release Notes

v2.1.75 — March 13, 2026

Link: Release v2.1.75

  • Opus 4.6 uses a 1M context window by default on Max, Team, and Enterprise plans
  • Tool permission denials prompt for a reason when intent is unclear
  • Memory files show last-modified timestamps
  • Bug fixes: voice mode, header model name display, session crashes, Bash ! mangling

v2.1.76 — March 2026

Link: Release v2.1.76

  • Added /effort command (low/medium/high/max)
  • Opus 4.6 defaults to medium effort for Max and Team
  • "ultrathink" keyword re-introduced for high effort
  • Opus 4 and 4.1 removed — users auto-moved to Opus 4.6
  • Default Opus model on Bedrock/Vertex/Foundry changed to Opus 4.6
  • Fixed: spurious "Context limit reached" errors on 1M sessions
  • Fixed: "adaptive thinking is not supported" errors with non-standard model strings
  • Fixed: auto-compaction retrying indefinitely (circuit breaker after 3 attempts)

v2.1.77 — March 2026

Link: Release v2.1.77

  • Minified function names changed (telemetry_gate: ZvCv, analytics_gate: u1_o1_, statsig_gate: Aihi, extra_usage_check: bf7Ij7, feedback_gate: L8Tv8T)
  • This patch script supports v2.1.77 via KNOWN_VERSIONS["2.1.77"]

User Circumstances and Troubleshooting Performed

Setup

  • Plan: Claude Max Account (confirmed via /status)
  • Claude Code version: 2.1.76 (confirmed latest via claude --version)
  • Model: Default Opus 4.6 (confirmed via /status — "Default Opus 4.6 - Most capable for complex work")
  • Login method: Claude Max Account (confirmed via /status)
  • Platform: macOS (Darwin 25.2.0)
  • Shell: zsh
  • API provider: First-party Anthropic (not Bedrock, Vertex, or Foundry)
  • Setting sources: User settings, Project local settings

Settings Present in Config

~/.claude/settings.json contains:

{
  "env": {
    "DISABLE_ERROR_REPORTING": "1",
    "DISABLE_TELEMETRY": "1"
  }
}

DISABLE_TELEMETRY is the root cause. DISABLE_ERROR_REPORTING is not in Pv() and does not trigger this bug independently.

No availableModels restrictions, no model overrides, no context-related settings in any settings file (~/.claude/settings.json, ~/.claude/settings.local.json, .claude/settings.local.json).

Observed Behavior

  1. /context displays claude-opus-4-6 . 23k/200k tokens (12%) — capped at 200k
  2. /model opus[1m] returns: "Opus 4.6 with 1M context is not available for your account"
  3. /remote-control is unavailable — confirmed same root cause per #29580 and #34083

What Was Tried

  1. Checked Claude Code version — confirmed 2.1.76 (latest), includes 1M support added in v2.1.75
  2. Switched model to Opus via /model default — Opus 4.6 selected but /context still showed 200k
  3. Attempted /model opus[1m] — rejected with error above
  4. Checked all context-related environment variablesCLAUDE_CODE_DISABLE_1M_CONTEXT, ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_MODEL, CLAUDE_CODE_MAX_CONTEXT, DISABLE_PROMPT_CACHING, all DISABLE_PROMPT_CACHING_* — all unset
  5. Inspected all settings files — no availableModels restrictions, no model overrides in any of the 3 settings files
  6. Verified no third-party provider config — no Bedrock/Vertex/Foundry env vars set
  7. Searched web and GitHub — found root cause documented across issues #34143, #34083, #29580
  8. Read official docs — confirmed Max plans should get Opus 1M automatically; docs do not mention DISABLE_TELEMETRY blocks this
  9. Identified root causeDISABLE_TELEMETRY causes Pv() in cli.js to bypass all feature-gate evaluation

GitHub Issues

Telemetry / Feature-Gate Bug (Root Cause)

Issue Title Date
#29580 DISABLE_TELEMETRY=1 breaks remote-control with misleading "not yet enabled" error (includes source code trace) Feb 28, 2026
#34083 Max plan: 1M context window shows 200k, Remote Control unavailable Mar 13, 2026
#34143 Opus 4.6 shows 200K context instead of 1M on Claude Max plan (v2.1.75) Mar 13, 2026
#34435 Opus 4.6 default model alias not using 1M context window (v2.1.76, Max) Mar 14, 2026
#33119 Remote Control not enabled on Max plan account Mar 2026

Early Opus 4.6 / 1M Context Bugs (Feb 2026, pre-GA)

Issue Title Date
#23432 /context command reports 200K max for Opus 4.6 instead of 1M Feb 5, 2026
#23472 Opus 4.6 with [1m] returns "long context beta not available" on Max subscription Feb 2026
#23700 Long context beta unavailable for Max plan with Opus 4.6 1M Feb 2026
#23714 Opus 4.6 implies 1M but hard-caps at 200K — session crashes (Max 20x) Feb 2026
#23879 Bring 1M context window to Max plan subscribers Feb 2026
#24208 Opus 4.6 context display shows 200k instead of 1M Feb 2026

Regressions and Billing Issues (Feb-Mar 2026)

Issue Title Date
#26428 Sonnet 1M disappeared after update to v2.1.45 — regression on Max ($200/mo) Feb 2026
#28723 Unclear how 1M context billing works on Max subscription Mar 2026
#28927 Silent billing change in v2.1.51: 1M moved to extra-usage-only without notice Mar 2026
#28975 Opus 1M hits "Rate limit reached" on Max and Team plans Mar 2026
#29330 Opus 1M suddenly returns "Rate limit reached" on Max and Team (200K works fine) Mar 2026
#33154 Cowork forces [1m] causing instant rate limit errors on Max Mar 11, 2026
#33584 Feature request: mid-tier context (400K) at standard Max rates Mar 12, 2026

Related Telemetry Issues

Issue Title Date
#19117 Telemetry Configuration Ambiguity: Confusion between Statsig metrics and OpenTelemetry 2026
#10494 DISABLE_TELEMETRY flag ignored — Claude Code connects to Google despite opt-out 2025
#5508 OpenTelemetry telemetry cannot be disabled on Windows 2025

Original Feature Request

Issue Title Date
#5644 Feature Request: Enable 1M Context Window for Claude Code Aug 2025

Bedrock-Specific

Issue Title Date
#32673 Bedrock: 1M context should auto-enable for capable models Mar 2026

Key User Reports from Issue Comments

Telemetry as root cause (confirmed)

From #34143:

  • @Hoernchen: First identified the root cause — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC or DISABLE_TELEMETRY=1 blocks 1M context (6 thumbs up).
  • @davidlandais: Confirmed the fix. "It's a shame you have to disable this option to get access to 1M context. Not very honest."
  • @ZenAlexa: Confirmed on Windows 11 + v2.1.76 + Max plan. "Users shouldn't have to opt into telemetry to access a paid feature."

From #34083:

  • @lopi-py: Enabling telemetry fixed both 1M context and /remote-control.
  • @cwilso03: Confirmed removing DISABLE_TELEMETRY from settings.json immediately restored 1M. Adding it back removed 1M again. Provided the exact block to look for:
    "env": {
      "DISABLE_TELEMETRY": "1"
    }
  • @certiv-dwager: Speculates staged rollout — "they want to watch their metrics closely. so rolling out to telemetry enabled users first."
  • @inevity: Setting CLAUDE_CODE_ENABLE_TELEMETRY=1 did NOT fix it — the DISABLE_TELEMETRY key must be removed entirely.

From #29580:

  • @Tuxerino420: Full source code trace of Pv()qA()Ar(). Discovered DISABLE_TELEMETRY=0 also triggers the bug (!!"0" is true). The /remote-control command was completely hidden from /help, not just failing.
  • @jschneider: First reported the /remote-control variant. Confirmed only DISABLE_TELEMETRY was needed to trigger — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC was NOT set.

Additional factors (telemetry may not be the only cause)

From #34143:

  • @AspireAI-Developer: On Max plan, v2.1.76, macOS — verified none of the known env vars are set, still capped at 200K. Suggests additional factors beyond telemetry.
  • @ManufactoryOfCode: Workaround for non-telemetry cases — switch to Haiku in /model, then re-open /model and Opus 4.6 (1M) may appear as a separate option.
  • @chrisgagne: Used 1M with extra usage disabled, but after several minutes the 1M options disappeared from /model.

From #34083:

  • @stripped-down: Same account, same Max plan, same v2.1.76 — Linux VM gets 1M immediately, macOS still shows 200K (telemetry enabled on both). Suggests platform-gated rollout may also be a factor.
  • @Nantris: "It's just random. I use it across several machines with identical settings and some have 1M and some don't."
  • @kexgev: Had 1M yesterday, Max plan expired and re-purchased, now shows 200K again.

Pre-GA context and performance notes

From #23432:

  • @nerdpudding: Got 1M working on Max with extra usage enabled, but performance degraded past 250K tokens — slower responses and more errors past 350K. Noted the "Billed as extra usage" warning when switching to opus[1m].
  • @Notum: Before GA, 1M beta was only for API users with Tier 3+ plans, not Max subscribers — despite Max marketing listing 1M as a feature.
  • @felipekj: Max 20x subscriber, extra usage enabled, v2.1.49 — still got "The long context beta is not yet available for this subscription."
  • @peixotorms: Had 1M for several days on Max 20x, then it reverted to 200K without any config change.
  • @OZmasterAI: Provided a statusline workaround script to recalculate context percentage from actual token counts when the reported window is wrong.

Recommended Actions

  1. Comment on #34083 (oldest post-GA issue, March 13) documenting this case and the root cause

  2. Request Anthropic decouple telemetry from feature gates — the fix is straightforward (use w1() instead of !! in Pv(), or better, separate the code paths entirely)

  3. Do NOT remove DISABLE_TELEMETRY — telemetry opt-out is a legitimate privacy choice and should not be required for paid features

  4. Try re-authenticating as a long shot (won't fix the telemetry root cause but rules out stale auth):

    claude logout
    claude login
  5. Monitor these issues for an official fix:

    • #34083 — Primary post-GA issue (1M + remote-control)
    • #34143 — Most community-confirmed root cause analysis
    • #29580 — Source code trace proving the bug

References

Official

Community / Third-Party

#!/usr/bin/env python3
"""
Analyze Claude Code binary for the DISABLE_TELEMETRY feature-gate bug.
Reusable investigation tool for examining the Bun-compiled Mach-O binary,
finding function signatures, tracing call chains, verifying patch status,
analyzing network traffic, and inspecting Statsig feature gate caches.
Usage:
python3 analyze_claude_binary.py overview # Binary structure and key strings
python3 analyze_claude_binary.py functions # Extract key function bodies
python3 analyze_claude_binary.py callers # Map call sites for key functions
python3 analyze_claude_binary.py offsets # Byte offsets of all key patterns
python3 analyze_claude_binary.py patch-status # Quick patched/unpatched check (+ backups)
python3 analyze_claude_binary.py verify-patch # Full patch + telemetry verification
python3 analyze_claude_binary.py traffic-analysis # Network traffic allowed/blocked
python3 analyze_claude_binary.py cache # Statsig cache and settings.json
python3 analyze_claude_binary.py telemetry-paths # Trace all event sending paths
python3 analyze_claude_binary.py macho # Mach-O segment layout (otool)
python3 analyze_claude_binary.py version # Version detection and comparison
python3 analyze_claude_binary.py growthbook # GrowthBook SDK deep-dive
python3 analyze_claude_binary.py segment # Segment SDK deep-dive
python3 analyze_claude_binary.py model-gates # 1M context model validation paths
python3 analyze_claude_binary.py data-collection # Data-collection flag code contexts
python3 analyze_claude_binary.py search PATTERN # Find byte offsets of a string
python3 analyze_claude_binary.py context PATTERN # Show context around pattern
python3 analyze_claude_binary.py all # Run all commands in sequence
DATA COLLECTION FLAGS (v2.1.76):
Flag | Controlled by | After patch
----------------------------------------|-------------------|---------------------------
DISABLE_TELEMETRY | env-controlled | Zv()/u1_()/L8T() intact
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC| env-controlled | Zv()/b27()/BKK() intact
DISABLE_ERROR_REPORTING | env-controlled | $_() NOT touched
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY | env-controlled | L8T()+env NOT touched
DISABLE_FEEDBACK_COMMAND | env-controlled | command gate NOT touched
DISABLE_BUG_COMMAND | env-controlled | command gate NOT touched
DISABLE_AUTOUPDATER | env-controlled | WO_() NOT touched
DISABLE_COST_WARNINGS | env-controlled | separate path NOT touched
Patches applied (Ai + bf7 only):
Ai() — decouples feature gate evaluation from analytics state
bf7() — fixes 1M context race condition (undefined → true, not false)
All env flags remain fully respected after patching.
"""
import argparse
import glob
import json
import os
import subprocess
import sys
# All analysis targets this specific version. Minified function names change
# between versions, so patterns may not match a different binary.
EXPECTED_VERSION = "2.1.76"
# Number of JS bundle copies in the Bun-compiled Mach-O binary.
EXPECTED_COPY_COUNT = 2
# Context window overrides — None means "use the per-call default".
# Set by --before / --after CLI flags in main().
_CTX_BEFORE = None # bytes of context before a matched pattern
_CTX_AFTER = None # bytes of context after a matched pattern
def find_binary(override=None):
"""Follow symlink from ~/.local/bin/claude to the actual binary."""
if override:
path = os.path.realpath(override)
if not os.path.isfile(path):
print(f"ERROR: File not found: {path}", file=sys.stderr)
sys.exit(1)
return path
link = os.path.expanduser("~/.local/bin/claude")
if not os.path.exists(link):
print(f"ERROR: Claude CLI not found at {link}", file=sys.stderr)
print(" Install Claude Code or pass --binary /path/to/claude", file=sys.stderr)
sys.exit(1)
return os.path.realpath(link)
def read_binary(path):
"""Read binary into memory."""
with open(path, "rb") as f:
return f.read()
def find_all(data, pattern, limit=None):
"""Find all byte offsets of a pattern in data."""
offsets = []
start = 0
while True:
idx = data.find(pattern, start)
if idx == -1:
break
offsets.append(idx)
if limit and len(offsets) >= limit:
break
start = idx + 1
return offsets
def extract_context(data, offset, before=120, after=80):
"""Extract a text window around a byte offset.
Respects global _CTX_BEFORE / _CTX_AFTER overrides if set via --before/--after.
"""
actual_before = _CTX_BEFORE if _CTX_BEFORE is not None else before
actual_after = _CTX_AFTER if _CTX_AFTER is not None else after
start = max(0, offset - actual_before)
end = min(len(data), offset + actual_after)
return data[start:end].decode("utf-8", errors="replace")
def _fwd_ctx(data, offset, default_after=200):
"""Forward context: N bytes starting at offset (no before-context).
Respects global _CTX_AFTER override if set via --after.
Returns (text, bytes_shown).
"""
actual_after = _CTX_AFTER if _CTX_AFTER is not None else default_after
end = min(len(data), offset + actual_after)
text = data[offset:end].decode("utf-8", errors="replace")
return text, end - offset
def _show_func(data, label, pattern, context_after=400):
"""Helper: find ALL occurrences of pattern, print label and context for each.
Prints 'NOT FOUND' if pattern is absent — never silently skips.
Respects global _CTX_AFTER override if set via --after.
Returns the offset of the first occurrence, or -1.
"""
offsets = find_all(data, pattern)
count = len(offsets)
actual_after = _CTX_AFTER if _CTX_AFTER is not None else context_after
pat_str = pattern.decode(errors="replace")
print(f"--- {label} ({count} copies, {actual_after} bytes after match) ---")
print(f" Pattern: {pat_str}")
if offsets:
for i, idx in enumerate(offsets):
end = min(len(data), idx + actual_after)
shown = end - idx
ctx = data[idx:end].decode("utf-8", errors="replace")
print(f" Copy {i + 1} @{idx} (0x{idx:x}) [{shown} bytes]: {ctx}")
else:
print(f" NOT FOUND — this may indicate a different binary version than v{EXPECTED_VERSION}.")
return offsets[0] if offsets else -1
# ─── Subcommands ──────────────────────────────────────────────────────────────
def cmd_overview(data, binary_path):
"""Binary structure overview: file type, size, JS detection, string counts."""
print("=== Binary Overview ===")
print(f"Path: {binary_path}")
print(f"Size: {len(data):,} bytes ({len(data) / (1024*1024):.1f} MB)")
# File type
result = subprocess.run(["file", binary_path], capture_output=True, text=True)
print(f"Type: {result.stdout.strip().split(': ', 1)[-1]}")
# JS plaintext detection
func_count = data.count(b"function ")
print(f"JS plaintext: {'Yes' if func_count > 1000 else 'No'} ({func_count:,} 'function ' occurrences)")
# Key string counts
print("\nKey string counts:")
for label, pattern in [
("DISABLE_TELEMETRY", b"DISABLE_TELEMETRY"),
("DISABLE_ERROR_REPORTING", b"DISABLE_ERROR_REPORTING"),
("NONESSENTIAL_TRAFFIC", b"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
("process.env.", b"process.env."),
("GrowthBook/Statsig", b"Statsig"),
]:
count = data.count(pattern)
print(f" {label}: {count}")
# Codesign status
result = subprocess.run(
["codesign", "-v", binary_path], capture_output=True, text=True
)
print(f"\nCodesign: {'VALID' if result.returncode == 0 else 'INVALID'}")
if result.stderr.strip():
print(f" {result.stderr.strip()}")
def cmd_functions(data, _binary_path):
"""Extract key functions: Zv, L8T, u1_, Ai, BKK, hZ_, La_, Q, $_."""
print("=== Key Functions ===\n")
funcs = [
("Zv", b"function Zv(){", b"}function L8T"),
("L8T", b"function L8T(){", b"}var "),
("u1_", b"function u1_(){", b"}"),
("Ai", b"function Ai(){", b"}"),
("BKK", b"function BKK(){", b"}"),
("hZ_", b"function hZ_(_,T){", b"}function"),
("La_", b"function La_(_){", b"}async"),
]
for name, start_sig, end_sig in funcs:
offsets = find_all(data, start_sig)
print(f"{name}() — {len(offsets)} copies")
for i, off in enumerate(offsets):
end = data.find(end_sig, off + len(start_sig))
if end >= 0:
func_bytes = data[off : end + 1]
func_text = func_bytes.decode("utf-8", errors="replace")
print(f" Copy {i+1} @{off}: {func_text}")
else:
print(f" Copy {i+1} @{off}: <end marker '{end_sig.decode(errors='replace')}' not found>")
if not offsets:
print(f" NOT FOUND — may indicate a different binary version than v{EXPECTED_VERSION}")
print()
# Q() — special handling (short function)
q_sig = b"function Q(_,T){if(nH_===null)"
offsets = find_all(data, q_sig)
print(f"Q() (Statsig logger) — {len(offsets)} copies")
for i, off in enumerate(offsets):
end = data.find(b"}", off + 50)
end2 = data.find(b"}", end + 1) if end >= 0 else -1
if end2 >= 0:
func_text = data[off : end2 + 1].decode("utf-8", errors="replace")
print(f" Copy {i+1} @{off}: {func_text}")
if not offsets:
print(f" NOT FOUND — may indicate a different binary version than v{EXPECTED_VERSION}")
print()
# b27 Segment init — uses assignment syntax, not function declaration
b27_sig = b"b27=_q(async()=>{"
offsets = find_all(data, b27_sig)
print(f"b27 (Segment init) — {len(offsets)} copies")
for i, off in enumerate(offsets):
end = data.find(b"})", off + 30)
if end >= 0:
func_text = data[off:end + 2].decode("utf-8", errors="replace")
print(f" Copy {i+1} @{off}: {func_text}")
else:
end_marker = "'})'"
print(f" Copy {i+1} @{off}: <end marker {end_marker} not found>")
if not offsets:
print(f" NOT FOUND — may indicate a different binary version than v{EXPECTED_VERSION}")
print()
# Kaq GrowthBook creation
kaq_sig = b"Kaq=_q(()=>{"
offsets = find_all(data, kaq_sig)
print(f"Kaq (GrowthBook init) — {len(offsets)} copies")
for i, off in enumerate(offsets):
ctx, shown = _fwd_ctx(data, off, 400)
print(f" Copy {i+1} @{off} (0x{off:x}) [{shown} bytes]: {ctx}")
if not offsets:
print(f" NOT FOUND — may indicate a different binary version than v{EXPECTED_VERSION}")
print()
# $_ error reporting guard
err_sig = b"process.env.DISABLE_ERROR_REPORTING"
offsets = find_all(data, err_sig)
print(f"DISABLE_ERROR_REPORTING references — {len(offsets)} occurrences")
for i, off in enumerate(offsets):
ctx = extract_context(data, off, before=80, after=60)
print(f" @{off}: ...{ctx}...")
def cmd_callers(data, _binary_path):
"""Map all callers of Zv(), u1_(), Ai(), L8T(), BKK(), hZ_(), b27()."""
print("=== Call Chain Analysis ===\n")
targets = [
("Zv()", [b"Zv()", b"!Zv()", b"Zv()?"]),
("u1_()", [b"u1_()", b"!u1_()"]),
("Ai()", [b"Ai()", b"!Ai()"]),
("L8T()", [b"L8T()", b"!L8T()"]),
("BKK()", [b"BKK()", b"!BKK()", b"await BKK()"]),
("hZ_()", [b"hZ_(", b"hZ_(_"]),
("b27()", [b"b27()", b"await b27()"]),
]
for name, patterns in targets:
print(f"--- Callers of {name} ---")
for pat in patterns:
offsets = find_all(data, pat)
if not offsets:
continue
print(f" Pattern '{pat.decode()}': {len(offsets)} occurrences")
for off in offsets:
ctx = extract_context(data, off, before=50, after=80)
print(f" @{off} (0x{off:x}): ...{ctx}...")
print()
def cmd_offsets(data, _binary_path):
"""Show byte offsets of all DISABLE_TELEMETRY and related patterns."""
print("=== Byte Offsets ===\n")
patterns = [
("DISABLE_TELEMETRY (raw string)", b"DISABLE_TELEMETRY"),
("!!process.env.DISABLE_TELEMETRY", b"!!process.env.DISABLE_TELEMETRY"),
("0&process.env.DISABLE_TELEMETRY", b"0&process.env.DISABLE_TELEMETRY"),
("!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", b"!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
("0&process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", b"0&process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
("function Zv(){", b"function Zv(){"),
("function L8T(){", b"function L8T(){"),
("function u1_(){", b"function u1_(){"),
("function Ai(){", b"function Ai(){"),
("function Ai(){return!1||!0} (patched)", b"function Ai(){return!1||!0}"),
("function BKK(){", b"function BKK(){"),
("b27 unpatched (await BKK)", b"b27=_q(async()=>{if(!await BKK())return null"),
("b27 patched (await(0&&0))", b"b27=_q(async()=>{if(!await(0&&0))return null"),
]
for label, pat in patterns:
offsets = find_all(data, pat)
if offsets:
print(f"{label}: {len(offsets)} occurrences")
for off in offsets:
print(f" {off} (0x{off:x})")
else:
print(f"{label}: not found")
print()
def cmd_patch_status(data, binary_path):
"""Check whether the binary is patched, unpatched, or in an unknown state.
Minimal 2-patch approach: only Ai() and bf7() are patched.
Zv(), u1_(), b27(), L8T() must be INTACT — patching them is collateral damage
that removes env-var control over data collection.
"""
print("=== Patch Status ===\n")
print("Minimal patch approach: Ai() + bf7() patched, everything else intact.\n")
# Show backup files
bak_pattern = f"{binary_path}.bak.*"
backups = sorted(glob.glob(bak_pattern))
if backups:
print(f"=== Backup Files ({len(backups)}) ===")
for i, bak in enumerate(backups):
bak_size_mb = os.path.getsize(bak) / (1024 * 1024)
label = "ORIGINAL (true unpatched binary)" if i == 0 else f"backup #{i + 1}"
print(f" {bak} ({bak_size_mb:.1f} MB) — {label}")
print()
else:
print("No backups found — binary has never been patched with --real-run.\n")
# Functions that SHOULD be patched
print("--- Should be PATCHED (2 copies each) ---")
should_patch = [
("Ai() patched (return!1||!0) — feature gates enabled", b"function Ai(){return!1||!0}"),
("bf7() patched (return!0 for undefined) — 1M race condition fixed", b"function bf7(){let _=DT().cachedExtraUsageDisabledReason;if(_===void 0)return!0;"),
]
for label, pat in should_patch:
count = data.count(pat)
ok = count == EXPECTED_COPY_COUNT
status = " ✓ PATCHED" if ok else f" ✗ MISSING ({count} copies, expected {EXPECTED_COPY_COUNT})"
print(f" {label}: {count}{status}")
# Functions that MUST be intact (env-var controlled)
print("\n--- Must be INTACT (env-var controlled — 2 copies each) ---")
must_intact = [
("Zv() intact (!! for DISABLE_TELEMETRY) — analytics/Segment env-controlled",
b"s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY"),
("u1_() intact (return!Zv()) — analytics gate env-controlled",
b"function u1_(){return!Zv()}"),
("b27 intact (await BKK()) — Segment env-controlled",
b"b27=_q(async()=>{if(!await BKK())return null"),
("L8T() intact (!! for DISABLE_TELEMETRY) — surveys env-controlled",
b"function L8T(){return!!process.env.DISABLE_TELEMETRY"),
]
for label, pat in must_intact:
count = data.count(pat)
ok = count == EXPECTED_COPY_COUNT
status = " ✓ INTACT" if ok else f" ✗ UNEXPECTED ({count} copies, expected {EXPECTED_COPY_COUNT})"
print(f" {label}: {count}{status}")
# Collateral damage check — old superseded patches
print("\n--- Collateral damage check (must be 0 — superseded approach) ---")
collateral = [
("Zv() patched 0& — COLLATERAL DAMAGE: env vars ignored",
b"s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||0&process.env.DISABLE_TELEMETRY"),
("u1_() patched return!1&&0 — COLLATERAL DAMAGE: analytics always off",
b"function u1_(){return!1&&0}"),
("b27 patched 0&&0 — COLLATERAL DAMAGE: Segment always blocked",
b"b27=_q(async()=>{if(!await(0&&0))return null"),
]
collateral_found = False
for label, pat in collateral:
count = data.count(pat)
if count > 0:
collateral_found = True
print(f" ✗ {label}: {count} copies found")
print(f" → Rollback and re-patch: python3 patch_claude_binary.py --rollback --real-run")
if not collateral_found:
print(" ✓ None found — env-var control is intact")
# Overall verdict
ai_patched = data.count(b"function Ai(){return!1||!0}") >= EXPECTED_COPY_COUNT
bf7_patched = data.count(b"function bf7(){let _=DT().cachedExtraUsageDisabledReason;if(_===void 0)return!0;") >= EXPECTED_COPY_COUNT
zv_intact = data.count(b"s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY") == EXPECTED_COPY_COUNT
u1_intact = data.count(b"function u1_(){return!Zv()}") == EXPECTED_COPY_COUNT
b27_intact = data.count(b"b27=_q(async()=>{if(!await BKK())return null") == EXPECTED_COPY_COUNT
zv_buggy = data.count(b"s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY") >= EXPECTED_COPY_COUNT
print()
if ai_patched and bf7_patched and zv_intact and u1_intact and b27_intact:
print("VERDICT: CORRECTLY PATCHED — Ai()+bf7() fixed, env vars intact")
elif not ai_patched and zv_buggy:
print("VERDICT: UNPATCHED — bug present, run: python3 patch_claude_binary.py --real-run")
elif ai_patched and not bf7_patched:
print("VERDICT: PARTIALLY PATCHED — Ai() done, bf7() missing (1M race condition not fixed)")
print(" Run: python3 patch_claude_binary.py --real-run")
elif collateral_found:
print("VERDICT: SUPERSEDED PATCHES FOUND — rollback and re-patch with minimal approach")
print(" Run: python3 patch_claude_binary.py --rollback --real-run")
print(" Then: python3 patch_claude_binary.py --real-run")
else:
print("VERDICT: UNKNOWN STATE — patterns don't match expected v{} layout".format(EXPECTED_VERSION))
print(" Run: python3 analyze_claude_binary.py version to check binary version")
def cmd_verify_patch(data, binary_path):
"""Comprehensive verification that patch is correct and telemetry stays disabled."""
print("=== Patch Verification (Telemetry & Privacy) ===\n")
all_ok = True
# 0. Version check
print("--- Version ---")
cmd_version(data, binary_path)
print()
# 1. Core patch status — minimal 2-patch approach
print("--- Applied Patches (must be present, 2 copies each) ---")
applied = [
("Ai() patched (return!1||!0) — gates decoupled from analytics",
b"function Ai(){return!1||!0}", EXPECTED_COPY_COUNT),
("bf7() patched (return!0 for undefined) — 1M race condition fixed",
b"function bf7(){let _=DT().cachedExtraUsageDisabledReason;if(_===void 0)return!0;", EXPECTED_COPY_COUNT),
]
for label, pat, expected in applied:
count = data.count(pat)
ok = count == expected
if not ok:
all_ok = False
print(f" {'OK' if ok else 'FAIL'}: {label}{count} copies (expected {expected})")
print("\n--- Intact Functions (must NOT be patched — env-var controlled, 2 copies each) ---")
intact = [
("Zv() intact (!! for DISABLE_TELEMETRY) — env-var drives analytics+Segment",
b"s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY", EXPECTED_COPY_COUNT),
("u1_() intact (return!Zv()) — analytics gate env-controlled",
b"function u1_(){return!Zv()}", EXPECTED_COPY_COUNT),
("b27 intact (await BKK()) — Segment env-controlled",
b"b27=_q(async()=>{if(!await BKK())return null", EXPECTED_COPY_COUNT),
("L8T() intact (!! preserved) — surveys env-controlled",
b"function L8T(){return!!process.env.DISABLE_TELEMETRY||!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}", EXPECTED_COPY_COUNT),
]
for label, pat, expected in intact:
count = data.count(pat)
ok = count == expected
if not ok:
all_ok = False
print(f" {'OK' if ok else 'FAIL'}: {label}{count} copies (expected {expected})")
print("\n--- Collateral damage check (superseded patches — must be 0) ---")
collateral = [
("Zv() patched 0& — removes env-var control",
b"s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||0&process.env.DISABLE_TELEMETRY"),
("u1_() patched return!1&&0 — analytics always off",
b"function u1_(){return!1&&0}"),
("b27 patched 0&&0 — Segment always blocked",
b"b27=_q(async()=>{if(!await(0&&0))return null"),
]
collateral_found = False
for label, pat in collateral:
count = data.count(pat)
if count > 0:
collateral_found = True
all_ok = False
print(f" FAIL: {label}{count} copies found (rollback + re-patch required)")
if not collateral_found:
print(" OK: No superseded patches found")
# 2. Analytics callers gated by u1_() (env-controlled via Zv())
print("\n--- Analytics Event Callers (gated by u1_() → off when DISABLE_TELEMETRY=1) ---")
callers = [
("Lh_ (1st-party events)", b"function Lh_(_,T={}){if(!u1_())return;"),
("Taq (1st-party events)", b"function Taq(_){if(!u1_())return;"),
("hw9 (1p event logging)", b"u1_())return;let T=qG(\"tengu_1p_event_batch_config\""),
("j6$ (event batching)", b"u1_()||!Hq_)return;"),
]
for label, pat in callers:
count = data.count(pat)
ok = count >= EXPECTED_COPY_COUNT
if not ok:
all_ok = False
print(f" {'OK' if ok else 'FAIL'}: {label} — guard present {count} times → {'DISABLED' if ok else 'CHECK NEEDED'}")
# 3. Segment analytics (blocked by b27 patch)
print("\n--- Segment Analytics ---")
seg_orig = data.count(b"b27=_q(async()=>{if(!await BKK())return null;")
seg_patched = data.count(b"b27=_q(async()=>{if(!await(0&&0))return null")
seg_gate = data.count(b'WY9="tengu_log_segment_events"')
if seg_orig >= EXPECTED_COPY_COUNT:
print(f" b27 INTACT: Segment blocked by BKK()→!Zv() when DISABLE_TELEMETRY=1 ({seg_orig} copies)")
print(f" Segment SDK will NOT initialize when DISABLE_TELEMETRY=1 (env-controlled)")
elif seg_patched >= EXPECTED_COPY_COUNT:
print(f" b27 PATCHED (superseded): Segment always blocked regardless of env var ({seg_patched} copies)")
print(f" WARNING: rollback and re-patch to restore env-var control")
else:
print(f" b27 UNKNOWN: neither intact nor patched pattern found")
print(f" Segment feature gate: tengu_log_segment_events ({seg_gate} refs)")
# 4. Datadog analytics
print("\n--- Datadog Analytics ---")
dd_gate = data.count(b'XY9="tengu_log_datadog_events"')
print(f" Datadog feature gate: tengu_log_datadog_events ({dd_gate} refs)")
print(f" Gate defaults to FALSE → Datadog events NOT sent")
# 5. Feedback surveys (L8T remains active)
print("\n--- Feedback Surveys ---")
l8t = data.count(b"function L8T(){return!!process.env.DISABLE_TELEMETRY||!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}")
ok = l8t == EXPECTED_COPY_COUNT
if not ok:
all_ok = False
print(f" {'OK' if ok else 'FAIL'}: L8T() still uses !! → returns true → surveys SUPPRESSED")
# 6. Sentry error reporting (separate code path)
print("\n--- Sentry Error Reporting ---")
err_refs = data.count(b"DISABLE_ERROR_REPORTING")
print(f" DISABLE_ERROR_REPORTING: {err_refs} references (independent of our patches)")
print(f" Error reporting controlled by separate env var — NOT affected by patch")
# 7. Statsig init (should proceed now)
print("\n--- Statsig Feature Gate Init ---")
print(f" Ai() = true (patched) → Oq() proceeds with gate evaluation")
print(f" Oq() fallback chain: overrides → Ai() check → in-memory → cached → default")
print(f" With Ai()=true, Statsig will initialize and cache gate values")
# 8. Runtime behavior summary
print(f"\n{'='*60}")
print("RUNTIME BEHAVIOR SUMMARY")
print(f"{'='*60}")
print(" When DISABLE_TELEMETRY=1 (env-controlled paths, all intact):")
print(" Zv() → true (!!DISABLE_TELEMETRY=true) → u1_()=false, BKK()=false")
print(" u1_() → false (!Zv()=false) → analytics callers bail out")
print(" BKK() → false (!Zv()=false) → Segment SDK skipped (b27 returns null)")
print(" L8T() → true (!!DISABLE_TELEMETRY=true) → feedback surveys suppressed")
print()
print(" Patched (always, regardless of env var):")
print(" Ai() → true (return!1||!0) → Statsig/Oq() lookups always proceed")
print(" bf7() → true for undefined → 1M not blocked during async load")
print()
print(" Result: feature gates work; analytics/Segment/surveys off per env var.")
print(" NOTE: Statsig gate evaluation contacts api.anthropic.com (required for gates).")
print()
if all_ok:
print("VERDICT: ALL CHECKS PASSED — telemetry disabled, feature gates enabled")
print("\nRun 'traffic-analysis' subcommand for detailed network traffic analysis.")
else:
print("VERDICT: SOME CHECKS FAILED — review output above")
print(" Run: python3 patch_claude_binary.py --real-run to apply missing patches")
def cmd_traffic_analysis(data, _binary_path):
"""Analyze what network traffic the patch allows/blocks and identify limitations."""
print("=== Network Traffic Analysis ===\n")
# 1. BKK() callers — what does nonessential traffic gate allow?
print("--- BKK() Callers (nonessential traffic gate) ---")
print("BKK() returns !Zv() = !false = true after patch (traffic allowed)\n")
idx = 0
seen = set()
while True:
idx = data.find(b"BKK()", idx)
if idx == -1:
break
# Skip the function definition itself (independent of context window size)
if b"function BKK()" in data[max(0, idx - 15):idx + 5]:
idx += 1
continue
ctx = extract_context(data, idx, before=120, after=120)
key = data[idx:idx + 60]
if key not in seen:
seen.add(key)
print(f" @{idx} (0x{idx:x}): ...{ctx}...")
print()
idx += 1
# 2. Segment analytics init
print("--- Segment Analytics (b27 init) ---")
idx = data.find(b"b27=_q(async()=>{if(!await BKK())")
patched_idx = data.find(b"b27=_q(async()=>{if(!await(0&&0))")
display_idx = patched_idx if patched_idx >= 0 else idx
if display_idx >= 0:
ctx, shown = _fwd_ctx(data, display_idx, 250)
print(f" @{display_idx} (0x{display_idx:x}) [{shown} bytes]: {ctx}")
seg_key_prod = data.find(b'"LKJN8LsLERHEOXkw487o7qCTFOrGPimI"')
print(f"\n Segment write key (production) found: {'Yes' if seg_key_prod >= 0 else 'No'}")
# Check b27 patch status
b27_patched = data.count(b"b27=_q(async()=>{if(!await(0&&0))return null") >= EXPECTED_COPY_COUNT
if b27_patched:
print(" STATUS: b27 PATCHED — Segment SDK never initializes, no connection made")
else:
print(" STATUS: b27 UNPATCHED — Segment SDK initializes (connects to analytics.segment.com)")
print(" Event sending requires MY9()=true → tengu_log_segment_events gate")
my9_gate = data.count(b'WY9="tengu_log_segment_events"')
print(f" MY9() checks feature gate: tengu_log_segment_events ({my9_gate} refs)")
print(" Gate default: FALSE → Segment events NOT sent, but SDK still connects")
print(" Fix: python3 patch_claude_binary.py --real-run (applies b27 patch)")
print()
# 3. Datadog analytics
print("--- Datadog Analytics ---")
dd_gate = data.count(b'XY9="tengu_log_datadog_events"')
print(f" kY9() checks feature gate: tengu_log_datadog_events ({dd_gate} refs)")
print(" Gate default: FALSE → Datadog events NOT sent")
print()
# 4. GrowthBook SDK — feature gate config fetch
print("--- GrowthBook SDK (feature gate configs) ---")
gb_url = data.find(b"cdn.growthbook.io")
print(f" GrowthBook CDN URL found: {'Yes' if gb_url >= 0 else 'No'}")
if gb_url >= 0:
ctx = extract_context(data, gb_url, before=40, after=80)
print(f" @{gb_url} (0x{gb_url:x}): ...{ctx}...")
print(" GrowthBook fetches feature gate configurations (READ-ONLY)")
print(" This is REQUIRED for 1M context and /remote-control to work")
print()
# 5. Statsig/PKK init
print("--- Statsig Init (PKK) ---")
idx = data.find(b"PKK=_q(async()=>{")
if idx != -1:
ctx, shown = _fwd_ctx(data, idx, 200)
print(f" @{idx} (0x{idx:x}) [{shown} bytes]: {ctx}")
print("\n PKK just sets _0T flag, minimal network activity")
print()
# 6. hZ_ gate exposure logging (LOCAL ONLY)
print("--- Gate Exposure Logging (hZ_) — LOCAL ONLY ---")
idx = data.find(b"function hZ_(")
if idx != -1:
ctx, shown = _fwd_ctx(data, idx, 300)
print(f" @{idx} (0x{idx:x}) [{shown} bytes]: {ctx}")
else:
print(f" NOT FOUND — may indicate a different binary version than v{EXPECTED_VERSION}")
print("\n hZ_ writes gate values to LOCAL cache (cachedGrowthBookFeatures) via UT()")
print(" Does NOT send data to network. Required for gate value persistence.")
print(" Exposure EVENTS go through La_() → Taq() → u1_()=false → BLOCKED")
print()
# 7. GrowthBook remoteEval (sends user attributes to api.anthropic.com)
print("--- GrowthBook remoteEval (api.anthropic.com) ---")
remote_eval = data.count(b"remoteEval:!0")
print(f" remoteEval:!0 found: {remote_eval} times")
print(" GrowthBook uses server-side evaluation via api.anthropic.com")
print(" Sends: {deviceId, sessionId, platform, orgUUID, accountUUID,")
print(" userType, subscriptionType, rateLimitTier}")
print(" Returns: feature gate values (1M context, /remote-control, etc.)")
print(" REQUIRED — this IS the feature gate evaluation. Same API as conversations.")
print(" CANNOT be blocked without breaking feature gates entirely.")
print()
# 8. Summary of limitations
print(f"{'='*60}")
print("TRAFFIC ANALYSIS SUMMARY")
print(f"{'='*60}")
print()
print("BLOCKED (no data sent):")
print(" - 1st-party analytics events (Lh_, Taq, hw9, j6$) — u1_()=false")
print(" - Feedback surveys — L8T()=true suppresses them")
print(" - Sentry error reports — DISABLE_ERROR_REPORTING still set")
print(" - Datadog events — tengu_log_datadog_events gate defaults false")
print(" - La_() experiment exposures — Taq() → u1_()=false → BLOCKED")
b27_patched = data.count(b"b27=_q(async()=>{if(!await(0&&0))return null") >= EXPECTED_COPY_COUNT
if b27_patched:
print(" - Segment SDK initialization — BLOCKED by b27 patch (returns null)")
else:
print(" - Segment events — tengu_log_segment_events gate defaults false")
print()
print("ALLOWED (required for feature gates):")
print(" 1. GrowthBook remoteEval (api.anthropic.com)")
print(" - Server-side gate evaluation, sends user metadata (not content)")
print(" - Same API endpoint as conversations — no additional exposure")
print(" - Required: YES — this IS how feature gates are evaluated")
print()
if not b27_patched:
print("ALLOWED (optional, patchable):")
print(" 2. Segment SDK initialization (analytics.segment.com)")
print(" - SDK connects but sends NO events (gate defaults false)")
print(" - Required: NO — side effect of BKK()=true")
print(" - Fix: python3 patch_claude_binary.py --real-run (applies b27 patch)")
print()
print("NOT A LIMITATION (previously misidentified):")
print(" - hZ_() gate exposure logging — LOCAL cache writes only, NOT network")
print(" - La_() experiment exposures — already BLOCKED by u1_()=false via Taq()")
def cmd_cache(_data, _binary_path):
"""Inspect Statsig/GrowthBook feature gate cache."""
print("=== Feature Gate Cache ===\n")
cache_paths = [
os.path.expanduser("~/.claude/.claude.json"),
os.path.expanduser("~/.claude/statsig_cache.json"),
]
found = False
for path in cache_paths:
if os.path.exists(path):
found = True
print(f"File: {path}")
print(f"Size: {os.path.getsize(path)} bytes")
print(f"Modified: {os.path.getmtime(path)}")
try:
with open(path) as f:
cache = json.load(f)
except (json.JSONDecodeError, PermissionError) as e:
print(f" ERROR reading: {e}")
continue
print(f"Total keys: {len(cache)}")
# Search for feature gate related keys
gate_terms = [
"1m", "context", "tengu", "ccr", "bridge", "remote",
"million", "telemetry", "long_context", "model",
]
print("\nRelevant keys:")
for key in sorted(cache.keys()):
low = key.lower()
if any(t in low for t in gate_terms):
val = cache[key]
val_str = json.dumps(val)
print(f" {key}: {val_str}")
# Check nested dicts
if isinstance(cache[key], dict):
for subkey in sorted(cache[key].keys()):
slow = subkey.lower()
if any(t in slow for t in gate_terms):
val = cache[key][subkey]
val_str = json.dumps(val)
print(f" {key}.{subkey}: {val_str}")
print()
if not found:
print("No cache files found at:")
for path in cache_paths:
print(f" {path}")
print("\nThe cache is created when Statsig initializes (requires Zv() to return false).")
print("If DISABLE_TELEMETRY was set, the cache was never populated.")
print("After patching, restart Claude Code — it should create/refresh the cache.")
# Also check settings.json for the env vars causing the bug
settings_path = os.path.expanduser("~/.claude/settings.json")
if os.path.exists(settings_path):
print(f"\n--- Settings ({settings_path}) ---")
try:
with open(settings_path) as f:
settings = json.load(f)
env = settings.get("env", {})
for key in ["DISABLE_TELEMETRY", "DISABLE_ERROR_REPORTING",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
"CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY",
"DISABLE_NON_ESSENTIAL_MODEL_CALLS"]:
if key in env:
print(f" {key} = {json.dumps(env[key])}")
if not any(k in env for k in ["DISABLE_TELEMETRY", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"]):
print(" (No telemetry env vars set — bug not triggered via settings)")
except (json.JSONDecodeError, PermissionError) as e:
print(f" ERROR reading: {e}")
def cmd_telemetry_paths(data, _binary_path):
"""Trace all telemetry/analytics event sending paths."""
print("=== Telemetry & Event Sending Paths ===\n")
print("--- Statsig client initialization (PKK) ---")
idx = data.find(b"PKK=_q(async()")
if idx >= 0:
ctx, shown = _fwd_ctx(data, idx, 200)
print(f" @{idx} (0x{idx:x}) [{shown} bytes]: {ctx}")
else:
print(" Not found")
print("\n--- Statsig client setter (w68/nH_) ---")
for pat in [b"function w68(_){", b"nH_=_,"]:
offsets = find_all(data, pat)
for off in offsets:
ctx = extract_context(data, off, before=10, after=100)
print(f" @{off}: ...{ctx}...")
print("\n--- Event logger Q() ---")
offsets = find_all(data, b"nH_.logEvent")
print(f" nH_.logEvent: {len(offsets)} occurrences")
for off in offsets:
ctx = extract_context(data, off, before=60, after=40)
print(f" @{off}: ...{ctx}...")
print(f"\n nH_.logEventAsync: {data.count(b'nH_.logEventAsync')} occurrences")
print("\n--- Custom event sender ($6$) ---")
idx = data.find(b"$6$(_,T,q={})")
if idx >= 0:
ctx = extract_context(data, idx, before=30, after=200)
print(f" @{idx} (0x{idx:x}): {ctx}")
print("\n--- Statsig flush (BKK → x27 → closeAndFlush) ---")
idx = data.find(b"function x27()")
if idx >= 0:
ctx, shown = _fwd_ctx(data, idx, 150)
print(f" @{idx} (0x{idx:x}) [{shown} bytes]: {ctx}")
print("\n--- Feedback survey guards (L8T callers) ---")
offsets = find_all(data, b"L8T()")
for off in offsets:
ctx = extract_context(data, off, before=60, after=40)
# Skip the function definition itself
if b"function L8T()" in data[max(0, off - 20) : off + 5]:
continue
print(f" @{off}: ...{ctx}...")
def cmd_macho(binary_path):
"""Show Mach-O segment and section layout."""
print("=== Mach-O Layout ===\n")
result = subprocess.run(
["otool", "-l", binary_path], capture_output=True, text=True
)
if result.returncode != 0:
print(f"ERROR: otool failed: {result.stderr}", file=sys.stderr)
return
# Parse and display segments and sections
lines = result.stdout.split("\n")
in_section = False
sect = ""
for line in lines:
stripped = line.strip()
if stripped.startswith("segname "):
seg = stripped.split()[-1]
print(f"\nSegment: {seg}")
if stripped.startswith("vmsize "):
size = int(stripped.split()[-1], 16)
if size > 0:
print(f" VM Size: {size:,} bytes ({size / (1024*1024):.1f} MB)")
if stripped.startswith("filesize "):
size = int(stripped.split()[-1])
if size > 0:
print(f" File Size: {size:,} bytes ({size / (1024*1024):.1f} MB)")
if stripped.startswith("sectname "):
sect = stripped.split()[-1]
in_section = True
if in_section and stripped.startswith("size "):
size = int(stripped.split()[-1], 16)
if size > 0:
print(f" Section {sect}: {size:,} bytes ({size / (1024*1024):.2f} MB)")
in_section = False
def cmd_version(data, binary_path):
"""Check binary version and compare to expected.
Detects version via `claude --version` and by counting the VERSION string
in the binary itself. Warns clearly on mismatch with actionable next steps.
"""
print("=== Version Detection ===\n")
# Run claude --version
try:
result = subprocess.run(
[binary_path, "--version"],
capture_output=True, text=True, timeout=15,
)
reported = result.stdout.strip() if result.returncode == 0 else "<failed>"
except (subprocess.TimeoutExpired, FileNotFoundError):
reported = "<unavailable>"
print(f" Reported version: {reported}")
print(f" Expected version: {EXPECTED_VERSION}")
# Count version string in binary
ver_pat = f'VERSION:"{EXPECTED_VERSION}"'.encode()
ver_count = data.count(ver_pat)
print(f' VERSION:"{EXPECTED_VERSION}" in binary: {ver_count} occurrences')
match = EXPECTED_VERSION in reported
print(f"\n {'MATCH' if match else 'MISMATCH'}: ", end="")
if match:
print("Binary version matches expected. Patches are designed for this version.")
else:
print("Binary version does NOT match expected!")
print(" Minified function names (Zv, u1_, Ai, L8T, BKK, b27) change between versions.")
print(" Run 'functions' and 'patch-status' to check if patterns still exist:")
print(f" python3 analyze_claude_binary.py functions")
print(f" python3 analyze_claude_binary.py patch-status")
print(" The patch script will abort if expected patterns are not found.")
return match
def cmd_growthbook(data, _binary_path):
"""Analyze GrowthBook SDK: remote eval, user attributes, exposure tracking.
Consolidates manual investigation commands:
python3 analyze_claude_binary.py context 'Kaq=_q(' --window 500
python3 analyze_claude_binary.py context 'function Vw9(' --window 400
python3 analyze_claude_binary.py search 'DD8="sdk-'
python3 analyze_claude_binary.py context 'function hZ_(' --window 300
python3 analyze_claude_binary.py context 'function La_(' --window 250
"""
print("=== GrowthBook SDK Analysis ===\n")
# 1. Kaq() — GrowthBook instance creation
_show_func(data, "Kaq() — GrowthBook Instance Creation", b"Kaq=_q(", 500)
print()
# 2. Client key
_show_func(data, "GrowthBook Client Key (DD8)", b'DD8="sdk-', 60)
print()
# 3. Vw9() user attributes (sent via remoteEval)
_show_func(data, "Vw9() — User Attributes Sent via remoteEval", b"function Vw9(", 400)
print(" These attributes are sent to api.anthropic.com for server-side gate evaluation.")
print(" Fields: deviceId, sessionId, platform, orgUUID, accountUUID, userType, subscriptionType, rateLimitTier")
print(" CANNOT BE BLOCKED — this IS the feature gate evaluation request.")
print()
# 4. hZ_() — local cache writer (NOT network)
_show_func(data, "hZ_() — Gate Value Local Cache (NOT network)", b"function hZ_(", 350)
print(" hZ_ writes to cachedGrowthBookFeatures via UT() — LOCAL disk only.")
print(" Does NOT send data to network. Required for gate value persistence between sessions.")
print()
# 5. La_() — exposure tracking (already blocked)
_show_func(data, "La_() — Experiment Exposure Tracking", b"function La_(", 250)
la_calls_taq = data.count(b"Taq({experimentId:")
print(f" La_() calls Taq(): {la_calls_taq} refs")
print(" Taq() checks if(!u1_())return → u1_()=false → BLOCKED by patch")
print()
# 6. trackingCallback
tc_count = data.count(b"trackingCallback")
print(f"--- GrowthBook trackingCallback ({tc_count} refs) ---")
print(" SDK internal callback, fires through La_() → Taq() → u1_()=false → BLOCKED")
if tc_count == 0:
print(f" WARNING: trackingCallback not found — may indicate different binary version than v{EXPECTED_VERSION}")
print()
# 7. Hi() — lazy GrowthBook getter
_show_func(data, "Hi() — Lazy GrowthBook Instance", b"Hi=_q(async", 200)
print()
# 8. Summary
print("--- GrowthBook Traffic Summary ---")
print(" REQUIRED (feature gates): remoteEval request to api.anthropic.com")
print(" Sends: deviceId, sessionId, platform, org/account UUIDs, subscription type")
print(" Returns: feature gate values (1M context, /remote-control, etc.)")
print(" LOCAL ONLY: hZ_() writes gate values to disk cache")
print(" BLOCKED: La_() exposure events via Taq() → u1_()=false")
def cmd_segment(data, _binary_path):
"""Analyze Segment SDK: initialization, event gates, patch status.
Consolidates manual investigation commands:
python3 analyze_claude_binary.py context 'b27=_q(async' --window 150
python3 analyze_claude_binary.py search 'LKJN8LsLERHEOXkw487o7qCTFOrGPimI'
python3 analyze_claude_binary.py context 'function MY9(' --window 200
python3 analyze_claude_binary.py context 'async function ZLq(' --window 150
python3 analyze_claude_binary.py context 'async function I27(' --window 150
python3 analyze_claude_binary.py context 'function z7$(' --window 200
"""
print("=== Segment SDK Analysis ===\n")
# 1. b27 init
b27_orig_ctx = b"b27=_q(async()=>{if(!await BKK())return null"
b27_patched_ctx = b"b27=_q(async()=>{if(!await(0&&0))return null"
orig_count = data.count(b27_orig_ctx)
patched_count = data.count(b27_patched_ctx)
_show_func(data, "b27 — Segment SDK Lazy Init", b"b27=_q(async", 250)
print(f" Original pattern: {orig_count} copies")
print(f" Patched pattern: {patched_count} copies")
if patched_count >= EXPECTED_COPY_COUNT:
print(f" Status: PATCHED (Segment blocked — SDK never initializes)")
elif orig_count >= EXPECTED_COPY_COUNT:
print(f" Status: UNPATCHED (Segment SDK initializes, connects to analytics.segment.com)")
print(f" Fix: python3 patch_claude_binary.py --real-run")
elif orig_count == 0 and patched_count == 0:
print(f" Status: UNKNOWN — neither pattern found (different binary version than v{EXPECTED_VERSION}?)")
else:
print(f" Status: PARTIAL — orig={orig_count}, patched={patched_count} (expected {EXPECTED_COPY_COUNT} each)")
print()
# 2. Write keys — production and development
print("--- Segment Write Keys ---")
for label, key in [
("Production", b'"LKJN8LsLERHEOXkw487o7qCTFOrGPimI"'),
("Development", b'"b64sf1kxwDGe1PiSAlv5ixuH0f509RKK"'),
]:
count = data.count(key)
print(f" {label}: {key.decode()}{count} occurrences ({'found' if count > 0 else 'NOT found'})")
print()
# 3. MY9 / WY9 event gate
_show_func(data, "MY9() — Segment Event Gate", b"function MY9(", 200)
wy9 = data.count(b'WY9="tengu_log_segment_events"')
print(f" Gate name: tengu_log_segment_events ({wy9} refs)")
print(" Gate default: FALSE → events NOT sent even if Segment SDK initializes")
if wy9 == 0:
print(f" WARNING: gate name not found — may indicate different binary version than v{EXPECTED_VERSION}")
print()
# 4. ZLq / I27 callers — bail if b27 returns null
print(f"--- Event Senders (bail if b27 returns null) ---")
for name, pat in [("ZLq (track)", b"async function ZLq("), ("I27 (identify)", b"async function I27(")]:
count = data.count(pat)
idx = data.find(pat)
if idx >= 0:
ctx, shown = _fwd_ctx(data, idx, 150)
print(f" {name} @{idx} (0x{idx:x}) ({count} copies) [{shown} bytes]: {ctx}")
else:
print(f" {name}: NOT FOUND — may indicate different binary version than v{EXPECTED_VERSION}")
print()
# 5. Dispatch wrapper z7$ — calls MY9→ZLq, kY9→_Lq, Lh_
_show_func(data, "z7$() — Event Dispatch Wrapper", b"function z7$(", 200)
print(" Flow: z7$() → MY9()→ZLq (Segment), kY9()→_Lq (Datadog), Lh_() (1st-party)")
print(" All three paths are independently gated.")
# 6. Summary
print(f"\n--- Summary ---")
if patched_count >= EXPECTED_COPY_COUNT:
print(" b27 PATCHED: Segment SDK never initializes, no connection to analytics.segment.com")
print(" ZLq() and I27() both bail when b27() returns null")
else:
print(" b27 UNPATCHED: Segment SDK initializes (connects to analytics.segment.com)")
print(" BUT sends NO events because tengu_log_segment_events gate defaults false")
print(" Fix: python3 patch_claude_binary.py --real-run")
def cmd_search(data, _binary_path, pattern, limit=20):
"""Search for a string pattern and show byte offsets."""
pat_bytes = pattern.encode("utf-8")
total = data.count(pat_bytes)
offsets = find_all(data, pat_bytes, limit=limit)
print(f"=== Search: '{pattern}' ===")
print(f"Found: {total} total occurrences", end="")
if total > limit:
print(f" (showing first {limit} — use --limit {total} to show all, or --output FILE to capture)", end="")
print("\n")
for off in offsets:
print(f" {off} (0x{off:x})")
if not offsets:
print(" No matches found.")
def cmd_context(data, _binary_path, pattern, window=200):
"""Search for a pattern and show surrounding context at each occurrence."""
pat_bytes = pattern.encode("utf-8")
offsets = find_all(data, pat_bytes)
print(f"=== Context Search: '{pattern}' ===")
print(f"Found: {len(offsets)} occurrences\n")
for off in offsets:
ctx = extract_context(data, off, before=window, after=window)
print(f"--- @{off} (0x{off:x}) ---")
print(ctx)
print()
if not offsets:
print(" No matches found.")
print(f" Try a shorter pattern or check binary version (expected v{EXPECTED_VERSION}).")
def cmd_model_gates(data, binary_path):
"""Show 1M context model validation code paths.
Displays the full call chain for /model opus[1m] and /model sonnet[1m],
including the availability checks and the bf7() race-condition function.
Useful for diagnosing why 1M context might not work after patching.
"""
print("=== 1M Context Model Validation Paths ===\n")
print(
"Call chain for /model opus[1m]:\n"
" DaK(model) → !eF() && !fD() → if both false, block opus[1m]\n"
" eF() → if we(): false | if s8(): bf7() | else: true\n"
" fD() → cobalt_compass gate (enabled by Ai() patch)\n"
" bf7() → checks cachedExtraUsageDisabledReason (PATCHED: undefined→true)\n\n"
"Call chain for /model sonnet[1m]:\n"
" faK(model) → !_Q() → if false, block sonnet[1m]\n"
" _Q() → identical to eF() — if we(): false | if s8(): bf7() | else: true\n\n"
"For Max plan users: s8()=false → eF()/_Q() return true → 1M always allowed.\n"
"For Pro/Team with extra usage: s8()=true → bf7() decides (race-condition patched).\n"
)
targets = [
("DaK() — Opus 1M block gate", b"function DaK("),
("faK() — Sonnet 1M block gate", b"function faK("),
("eF() — Opus 1M availability", b"function eF()"),
("_Q() — Sonnet 1M availability", b"function _Q()"),
("bf7() — extra-usage billing check (PATCHED)", b"function bf7()"),
("fD() — cobalt compass gate", b"function fD()"),
("s8() — extra-usage scope check", b"function s8()"),
("we() — CLAUDE_CODE_DISABLE_1M_CONTEXT", b"function we()"),
("coral_reef_sonnet — Sonnet auto-1M server flag", b"coral_reef_sonnet"),
("MG8() — Sonnet auto-1M via clientDataCache", b"function MG8()"),
("WE() — is Pro plan check", b"function WE()"),
]
for label, pat in targets:
_show_func(data, label, pat, context_after=300)
print()
def cmd_data_collection(data, binary_path):
"""Show code context for all data-collection flag names.
Searches for each of the 8 data-collection env var names used in v2.1.76
and prints the surrounding code context. Use this to verify which code
paths each flag controls and that no flags have been removed by patches.
"""
print("=== Data Collection Flag Code Contexts ===\n")
print(
"All flags should appear in their original code paths.\n"
"Flags listed as 'env-controlled' must NOT be altered by patches.\n"
"Patches: Ai()+bf7() only — none of these flags are patched.\n"
)
flags = [
("DISABLE_TELEMETRY", b"DISABLE_TELEMETRY"),
("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", b"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
("DISABLE_ERROR_REPORTING", b"DISABLE_ERROR_REPORTING"),
("CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY", b"CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY"),
("DISABLE_FEEDBACK_COMMAND", b"DISABLE_FEEDBACK_COMMAND"),
("DISABLE_BUG_COMMAND", b"DISABLE_BUG_COMMAND"),
("DISABLE_AUTOUPDATER", b"DISABLE_AUTOUPDATER"),
("DISABLE_COST_WARNINGS", b"DISABLE_COST_WARNINGS"),
]
for label, pat in flags:
offsets = find_all(data, pat)
count = len(offsets)
print(f"--- {label} ({count} occurrences) ---")
if count == 0:
print(" NOT FOUND — flag may not exist in this binary version.")
print()
continue
for off in offsets:
ctx = extract_context(data, off, before=80, after=120)
print(f" @{off}: ...{ctx}...")
print()
print()
def cmd_all(data, binary_path):
"""Run all analysis commands."""
cmd_version(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_overview(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_verify_patch(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_traffic_analysis(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_patch_status(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_functions(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_callers(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_offsets(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_telemetry_paths(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_growthbook(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_segment(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_model_gates(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_data_collection(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_cache(data, binary_path)
print("\n" + "=" * 70 + "\n")
cmd_macho(binary_path)
def main():
parser = argparse.ArgumentParser(
description="Analyze Claude Code binary for the DISABLE_TELEMETRY feature-gate bug.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
subcommands:
overview Binary structure: file type, size, JS plaintext detection,
key string counts (DISABLE_TELEMETRY, etc.), codesign status.
functions Extract and display key functions with their bodies:
Zv (feature gate bypass), L8T (feedback surveys), u1_ (analytics
gate), Ai (Statsig enabled), BKK (nonessential traffic), hZ_
(local cache), La_ (exposure tracking), b27 (Segment init),
Kaq (GrowthBook init), Q (Statsig logger), $_ (error reporting).
callers Map all call sites for Zv(), u1_(), Ai(), L8T(), BKK(), hZ_(),
b27() with surrounding context. Useful for tracing which code
paths depend on each function.
offsets Byte offsets of all DISABLE_TELEMETRY, NONESSENTIAL_TRAFFIC,
and key function patterns. Shows both patched and unpatched
variants including b27. Useful for manual binary inspection.
patch-status Quick check: is the binary patched, unpatched, or partially
patched? Shows per-function status for Zv, u1_, Ai, b27, L8T
with overall verdict.
verify-patch Comprehensive verification that all patches are correct AND
telemetry/analytics remain disabled. Checks core patches (Zv,
u1_, Ai, b27), all 4 analytics callers (Lh_, Taq, hw9, j6$),
Segment/Datadog gates, feedback surveys, Sentry, Statsig init,
and binary version. Includes runtime behavior summary.
traffic-analysis Detailed analysis of what network traffic the patch allows vs
blocks. Identifies BKK() callers, Segment SDK init (b27 patch
status), Datadog, GrowthBook remoteEval, gate exposure logging
(LOCAL only), and PKK/Statsig. Corrected: hZ_ is LOCAL cache,
La_→Taq→u1_ is BLOCKED.
cache Inspect Statsig/GrowthBook feature gate cache files
(~/.claude/.claude.json) and settings.json env vars. Shows gate
values for 1M context, remote-control, etc. Empty cache means
Statsig never initialized (common with DISABLE_TELEMETRY bug).
telemetry-paths Trace all telemetry/analytics event sending code paths: Statsig
init (PKK), client setter (w68/nH_), event logger (Q), custom
event sender ($6$), Statsig flush, and L8T callers.
macho Mach-O segment and section layout via otool -l. Shows __TEXT,
__DATA, __const sizes. The JS bundle lives in __TEXT.__const.
version Detect binary version, compare to expected v{ver}. Shows
version string count in binary. Warns on mismatch with
actionable next steps.
growthbook GrowthBook SDK deep-dive: instance creation (Kaq), client key,
user attributes (Vw9), local caching (hZ_), exposure tracking
(La_→Taq→blocked), trackingCallback chain, Hi() lazy init.
segment Segment SDK deep-dive: b27 init pattern and patch status, write
keys, MY9/WY9 event gate, ZLq/I27 callers, z7$ dispatch wrapper.
model-gates Show 1M context model validation code paths: DaK (Opus 1M
block), faK (Sonnet 1M block), eF() (Opus 1M availability),
_Q() (Sonnet 1M availability), bf7() (extra-usage billing
race-condition — patched), fD() (cobalt compass), s8()
(scope check), coral_reef_sonnet, MG8(), WE(). Key
diagnostic for "why isn't 1M working after patching?"
data-collection Show code context for all 8 data-collection env var flags
in v2.1.76: DISABLE_TELEMETRY, DISABLE_NONESSENTIAL_TRAFFIC,
DISABLE_ERROR_REPORTING, DISABLE_FEEDBACK_SURVEY,
DISABLE_FEEDBACK_COMMAND, DISABLE_BUG_COMMAND,
DISABLE_AUTOUPDATER, DISABLE_COST_WARNINGS. Verifies all
flags are present and unaltered (env-controlled).
search PATTERN Find all byte offsets of a string pattern in the binary.
Use --limit N to cap results (default 20 for search only).
Use --limit 0 to show all results.
context PATTERN Show surrounding context at each pattern occurrence.
Use --window N to set context size in bytes (default 200).
Supports multi-word patterns: context 'await BKK()'
all Run all analysis commands in sequence.
global options:
--before N Bytes of context before each matched pattern (overrides all
per-command defaults). Default varies per command (50–120).
--after N Bytes of context after each matched pattern (overrides all
per-command defaults, including _show_func context windows).
Default varies per command (80–400).
--output FILE Redirect all output to FILE instead of stdout. Useful for
commands with large output (data-collection, callers, all).
Example: python3 analyze_claude_binary.py all --output report.txt
Example: python3 analyze_claude_binary.py callers --before 200 --after 400 --output callers.txt
""".format(ver=EXPECTED_VERSION),
)
parser.add_argument(
"command",
choices=[
"overview", "functions", "callers", "offsets", "patch-status",
"verify-patch", "traffic-analysis", "cache", "telemetry-paths",
"macho", "version", "growthbook", "segment",
"model-gates", "data-collection",
"search", "context", "all",
],
help="Analysis subcommand",
)
parser.add_argument(
"pattern",
nargs="*",
default=[],
help="Search pattern (for 'search' and 'context' commands). Multi-word patterns supported.",
)
parser.add_argument(
"--binary",
type=str,
default=None,
help="Path to Claude binary (default: auto-detect via ~/.local/bin/claude symlink)",
)
parser.add_argument(
"--window",
type=int,
default=200,
help="Context window size in bytes (default: 200, for 'context' command)",
)
parser.add_argument(
"--limit",
type=int,
default=20,
help="Max results for 'search' command (default: 20; use 0 for unlimited)",
)
parser.add_argument(
"--before",
type=int,
default=None,
metavar="N",
help="Bytes of context BEFORE each matched pattern (overrides per-command defaults)",
)
parser.add_argument(
"--after",
type=int,
default=None,
metavar="N",
help="Bytes of context AFTER each matched pattern (overrides per-command defaults)",
)
parser.add_argument(
"--output",
type=str,
default=None,
metavar="FILE",
help="Write all output to FILE instead of stdout (useful for large commands like 'all')",
)
args = parser.parse_args()
# Join multi-word patterns (fixes: context 'await BKK' was split into 2 tokens)
pattern = " ".join(args.pattern) if args.pattern else None
if args.command in ("search", "context") and not pattern:
parser.error(f"'{args.command}' requires a PATTERN argument")
# Apply global context window overrides
global _CTX_BEFORE, _CTX_AFTER
if args.before is not None:
_CTX_BEFORE = args.before
if args.after is not None:
_CTX_AFTER = args.after
# Redirect stdout to file if --output was given
output_file = None
if args.output:
output_file = open(args.output, "w", encoding="utf-8")
sys.stdout = output_file
print(f"# Output captured from: python3 analyze_claude_binary.py {args.command}")
print(f"# Written to: {args.output}\n")
try:
binary_path = find_binary(args.binary)
# Commands that don't need binary data
if args.command == "macho":
cmd_macho(binary_path)
return
# Read binary
print(f"Loading {binary_path} ...\n")
data = read_binary(binary_path)
size_mb = len(data) / (1024 * 1024)
print(f"Binary: {binary_path}")
print(f" Size: {len(data):,} bytes ({size_mb:.1f} MB)")
print(f" Context window: --before {_CTX_BEFORE if _CTX_BEFORE is not None else '(per-command defaults)'}"
f", --after {_CTX_AFTER if _CTX_AFTER is not None else '(per-command defaults)'}")
print(f" Tip: use --before N --after M to widen/narrow all context windows")
print(f" use --output FILE to capture full output to a file")
print()
dispatch = {
"overview": lambda: cmd_overview(data, binary_path),
"functions": lambda: cmd_functions(data, binary_path),
"callers": lambda: cmd_callers(data, binary_path),
"offsets": lambda: cmd_offsets(data, binary_path),
"patch-status": lambda: cmd_patch_status(data, binary_path),
"verify-patch": lambda: cmd_verify_patch(data, binary_path),
"traffic-analysis": lambda: cmd_traffic_analysis(data, binary_path),
"cache": lambda: cmd_cache(data, binary_path),
"telemetry-paths": lambda: cmd_telemetry_paths(data, binary_path),
"version": lambda: cmd_version(data, binary_path),
"growthbook": lambda: cmd_growthbook(data, binary_path),
"segment": lambda: cmd_segment(data, binary_path),
"model-gates": lambda: cmd_model_gates(data, binary_path),
"data-collection": lambda: cmd_data_collection(data, binary_path),
"search": lambda: cmd_search(data, binary_path, pattern, args.limit),
"context": lambda: cmd_context(data, binary_path, pattern, args.window),
"all": lambda: cmd_all(data, binary_path),
}
dispatch[args.command]()
finally:
if output_file:
sys.stdout = sys.__stdout__
output_file.close()
print(f"Output written to: {args.output}")
if __name__ == "__main__":
main()

Plan: Binary Patch Claude Code v2.1.76 — Fix Telemetry/Feature-Gate Bug

Historical document. This is the original design plan describing an over-engineered 2-patch approach (Zv + u1_) that hardcoded analytics-off and Segment-blocked regardless of env vars. The actual patches implemented are different — only Ai() and bf7() are patched, leaving Zv(), u1_(), b27(), and L8T() intact so all data-collection flags remain fully env-controlled. See README.md for the current approach and rationale. The investigation methodology, binary analysis details, gotchas, and safety notes in this document remain accurate and useful for future version adaptation.

User Messages (exact quotes, in order)

  1. "make a plan to directly patch the binary so the code works correctly"
    • Context: After investigating the DISABLE_TELEMETRY bug blocking 1M context and /remote-control
  2. "also use the open cli command to open the key github issue urls i should do the thumbs up on now"
    • Context: Opened #29580, #34143, #34083 for thumbs-up reactions (completed)
  3. "make sure the patches will fix the bug so that all impacted features become enabled while telemetry and other data collection remains disabled"
    • Context: User wants features enabled but privacy preserved
  4. "make sure your plan includes how the changes are going to make would it be best to create a script to do the patch and should there be a backup so the changes are reversible"
    • Context: User wants a reusable script with backup/rollback
  5. "make a dir to put this code in and double check it for edge cases and failure modes and that it will work correctly and is easily set, also dry run must be the default and --real-run to actually make changes"
    • Context: Dry-run default, --real-run to apply. Script created at claude-patch/patch_claude_binary.py
  6. "also include the path to md source reference in the plan"
    • Context: Link to the investigation report MD file

Context

Claude Code v2.1.76 has a bug where DISABLE_TELEMETRY=1 in ~/.claude/settings.json disables ALL feature-gate evaluation (GrowthBook/Statsig), not just telemetry. This blocks 1M context, /remote-control, and all other gated features for Max plan users. The root cause is in the Zv() function which uses !! (truthy check) instead of s_() (proper boolean parser) for telemetry env vars.

Goal: Create a Python patch script with backup/rollback that enables all feature-gated capabilities (1M context, /remote-control) while keeping telemetry, analytics, feedback surveys, and error reporting disabled.

Investigation report: ~/source/general/2026_03_15_claude_code_1m_context_report.md

Binary Details

  • File: ~/.local/share/claude/versions/2.1.76 (182MB Mach-O arm64, Bun-compiled)
  • Symlink: ~/.local/bin/claude → above
  • JS is plaintext inside the binary (not compressed, stored in __const Mach-O section)
  • Two copies of the JS bundle exist in the binary (Bun artifact — both must be patched)

Functions to Patch

1. Zv() — Feature-gate bypass (THE BUG)

BEFORE (binary @ offsets 66124886 and 168259406):

function Zv(){return s_(process.env.CLAUDE_CODE_USE_BEDROCK)||s_(process.env.CLAUDE_CODE_USE_VERTEX)||s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY||!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}

AFTER:

function Zv(){return s_(process.env.CLAUDE_CODE_USE_BEDROCK)||s_(process.env.CLAUDE_CODE_USE_VERTEX)||s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||0&process.env.DISABLE_TELEMETRY||0&process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}

How it works: Replace !! with 0& (bitwise AND with 0). 0 & anything always evaluates to 0 (falsy) in JavaScript, regardless of the env var value. Same byte length (2 bytes each).

Verified !! offsets in Zv():

  • Copy 1: !! at bytes 66125029 and 66125062
  • Copy 2: !! at bytes 168259549 and 168259582

JS precedence is safe: & (precedence 8) binds tighter than || (precedence 3), so:

s_(X) || 0&process.env.DISABLE_TELEMETRY || 0&process.env.NONESSENTIAL

parses as:

s_(X) || (0 & process.env.DISABLE_TELEMETRY) || (0 & process.env.NONESSENTIAL)
= s_(X) || 0 || 0
= s_(X)

Result: Zv() returns true ONLY for Bedrock/Vertex/Foundry users. First-party Max users get feature flags evaluated.

2. u1_() — Analytics event gate (prevents re-enabling telemetry)

BEFORE (binary @ offsets 75395095 and 177529615):

function u1_(){return!Zv()}

Without this patch, fixing Zv() would cause u1_() to return true, re-enabling Statsig custom analytics event sending via $6$().

AFTER:

function u1_(){return!1&&0}

How it works: !1&&0 = false && 0 = false (short-circuit). Always returns false. Same byte length as !Zv() (5 bytes each: !Zv() vs !1&&0).

Verified offsets:

  • Copy 1: u1_ function at 75395095, body return!Zv() at bytes 75395110–75395121
  • Copy 2: u1_ function at 177529615, body return!Zv() at bytes 177529630–177529641

3. L8T() — NOT patched (intentional)

function L8T(){return!!process.env.DISABLE_TELEMETRY||!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}

L8T() is called by feedback survey logic (2 callers). Leaving it unpatched means feedback surveys stay suppressed.

4. $_() error reporting — NOT patched (intentional)

Uses process.env.DISABLE_ERROR_REPORTING directly — completely independent code path from Zv(). Sentry error reporting stays disabled.

5. BKK() — NOT patched (harmless)

function BKK(){if(Zv())return!1;return!0}

After patching Zv(), BKK() returns true for first-party users, enabling Statsig closeAndFlush() on exit. This is harmless because:

  • With u1_() patched, $6$() never sends custom analytics events
  • closeAndFlush() only flushes whatever events are queued
  • Gate exposure events (from feature flag lookups) are inherent to evaluating gates — they can't be disabled without also disabling the gates

6. Q() — Low-level Statsig logger — NOT patched (inherent to gates)

function Q(_,T){if(nH_===null){fe_.push({eventName:_,metadata:T,async:!1});return}nH_.logEvent(_,T)}

Q() has no guard — it logs any event passed to it. Gate exposure events flow through Q() as part of Statsig's feature flag evaluation. This is the minimum telemetry inherent to having working feature gates. Without these exposure events, Statsig can't track which gates are active — but this is read-side metadata, not usage analytics.

What Gets Enabled vs Stays Disabled

Feature After Patch Mechanism
1M context window ENABLED Zv() no longer blocks feature-gate eval
/remote-control ENABLED tengu_ccr_bridge flag now evaluated
All other gated features ENABLED GrowthBook/Statsig flags evaluated
Statsig feature flag fetch ENABLED Required to evaluate gates (minimal server read)
Custom analytics events ($6$) DISABLED u1_() patched to always return false
Feedback surveys DISABLED L8T() unpatched, still returns true
Sentry error reporting DISABLED $_() uses DISABLE_ERROR_REPORTING directly
Statsig gate exposure events MINIMAL Inherent to feature gate evaluation — cannot be separated

Tradeoff: Statsig initialization will make a network request to fetch feature gate configurations. This is the minimum server contact required for feature gates to work. Custom analytics events and feedback surveys remain fully disabled.

Implementation: Python Patch Script

File to create: patch_claude_binary.py

The script will:

  1. Auto-detect the Claude Code binary path (follows symlink from ~/.local/bin/claude)
  2. Create timestamped backup (e.g., 2.1.76.bak.20260315_224500) before patching
  3. Find patterns by content (not hardcoded offsets) — searches for the exact function signatures
  4. Validate expected replacement counts before writing (exactly 2 copies of each function)
  5. Context-aware matching — patches !! only inside Zv(), not in L8T() or elsewhere
  6. Re-sign the binary with codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime
  7. Verify the patched binary runs (claude --version)
  8. Support --rollback flag to restore the latest backup
  9. Support --dry-run flag to show what would be patched without modifying

Script logic (pseudocode):

#!/usr/bin/env python3
"""Patch Claude Code binary to fix DISABLE_TELEMETRY blocking feature gates."""

import argparse, os, shutil, subprocess, sys, time

def find_binary():
    """Follow symlink from ~/.local/bin/claude to actual binary."""
    link = os.path.expanduser("~/.local/bin/claude")
    return os.path.realpath(link)

def backup(path):
    """Create timestamped backup."""
    ts = time.strftime("%Y%m%d_%H%M%S")
    bak = f"{path}.bak.{ts}"
    shutil.copy2(path, bak)
    return bak

def patch_zv(data):
    """Patch Zv(): replace !! with 0& for DISABLE_TELEMETRY and NONESSENTIAL_TRAFFIC."""
    # Context signature to match only Zv(), not L8T():
    zv_marker = b's_(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY'
    zv_replacement = b's_(process.env.CLAUDE_CODE_USE_FOUNDRY)||0&process.env.DISABLE_TELEMETRY'

    count = data.count(zv_marker)
    assert count == 2, f"Expected 2 Zv() copies, found {count}"
    data = data.replace(zv_marker, zv_replacement)

    # Now patch the NONESSENTIAL_TRAFFIC !! that follows in the same function
    nt_marker = b'||!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}function L8T'
    nt_replacement = b'||0&process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}function L8T'

    count = data.count(nt_marker)
    assert count == 2, f"Expected 2 NONESSENTIAL patches, found {count}"
    data = data.replace(nt_marker, nt_replacement)

    return data

def patch_u1(data):
    """Patch u1_(): replace return!Zv() with return!1&&0."""
    marker = b'function u1_(){return!Zv()}'
    replacement = b'function u1_(){return!1&&0}'

    count = data.count(marker)
    assert count == 2, f"Expected 2 u1_() copies, found {count}"
    data = data.replace(marker, replacement)

    return data

def resign(path):
    """Ad-hoc codesign the binary."""
    subprocess.run([
        "codesign", "--sign", "-", "--force",
        "--preserve-metadata=entitlements,requirements,flags,runtime",
        path
    ], check=True)

def verify(path):
    """Check the binary runs."""
    result = subprocess.run([path, "--version"], capture_output=True, text=True, timeout=10)
    assert result.returncode == 0, f"Binary failed: {result.stderr}"
    print(f"  Version: {result.stdout.strip()}")

def rollback(path):
    """Restore latest backup."""
    # Find newest .bak.* file
    ...

def main():
    parser = argparse.ArgumentParser(...)
    parser.add_argument("--real-run", action="store_true", help="Apply patches (default is dry-run)")
    parser.add_argument("--rollback", action="store_true")
    args = parser.parse_args()
    dry_run = not args.real_run

    binary_path = find_binary()
    if args.rollback: rollback(binary_path); return

    data = open(binary_path, "rb").read()
    if check_already_patched(data): print("Already patched"); return
    if not check_needs_patching(data): print("ERROR: unknown binary version"); sys.exit(1)

    data = patch_zv(data)   # returns (data, patches) with assertion guards
    data = patch_u1(data)   # returns (data, patches) with assertion guards
    verify_no_collateral(data)  # confirms L8T() intact
    assert len(data) == original_size  # binary size unchanged

    if dry_run: print("[DRY RUN] No changes written"); return

    bak = backup(binary_path)
    open(binary_path, "wb").write(data)
    if not resign(binary_path): rollback_and_exit(bak)  # auto-rollback on codesign failure
    if not verify(binary_path): rollback_and_exit(bak)  # auto-rollback on verify failure

Key safety features:

  1. Pattern matching, not offset-based — finds functions by their unique surrounding context, immune to minor binary layout changes
  2. Assertion guards — script aborts if expected pattern counts don't match (e.g., if binary format changes in a future version)
  3. Context-aware Zv() patching — uses }function L8T as a suffix anchor to only patch the !! in Zv(), not the identical !! in L8T()
  4. Timestamped backup — never overwrites previous backups
  5. Ad-hoc codesign — macOS kernel kills (SIGKILL) any Mach-O binary with an invalid signature. Modifying bytes invalidates the original Anthropic signature. codesign --sign - creates an ad-hoc (local, no Apple Developer ID) signature. --preserve-metadata=entitlements,requirements,flags,runtime keeps the original entitlements (network access, hardened runtime) intact. Without re-signing, the patched binary won't launch at all.
  6. Auto-rollback on failure — if codesign or verification fails, the script restores from backup automatically
  7. Post-patch verification — runs claude --version to confirm binary still works

Execution Steps

Step 1: Script already created

~/source/general/claude-patch/patch_claude_binary.py

Step 2: Dry-run (default — no changes made)

cd ~/source/general/claude-patch
python3 patch_claude_binary.py

Step 3: Apply the patch (requires explicit --real-run)

python3 patch_claude_binary.py --real-run

Step 4: Verify in Claude Code (requires restart)

# Start a NEW Claude Code session, then:
# /context  → should show 1000k
# /model opus[1m]  → should succeed
# /help  → should list /remote-control

Rollback

# Show what would be restored (dry-run default):
python3 patch_claude_binary.py --rollback

# Actually restore:
python3 patch_claude_binary.py --rollback --real-run

# Or manually:
cp ~/.local/share/claude/versions/2.1.76.bak.TIMESTAMP ~/.local/share/claude/versions/2.1.76
codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime ~/.local/share/claude/versions/2.1.76

Investigation Methods

How the bug was found and the patch designed:

  1. strings <binary> | grep 'DISABLE_TELEMETRY' — confirmed JS is stored as plaintext inside the Bun-compiled Mach-O binary (not compressed), found all string references
  2. grep -boa 'DISABLE_TELEMETRY' <binary> — got exact byte offsets of every occurrence (9 total in v2.1.76, only 4 are in Zv/L8T)
  3. Python data.find() with context window — extracted surrounding JS at each byte offset to identify which function each occurrence belongs to (Zv vs L8T vs error reporting)
  4. otool -l <binary> — dumped Mach-O load commands to understand segment layout (__TEXT.__const holds the JS bundle, __TEXT.__jsc_int holds JavaScriptCore internals)
  5. file <binary> — confirmed Mach-O 64-bit executable arm64
  6. Pattern-based function identification — searched for unique context signatures like s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY to distinguish Zv() from L8T() (both contain !!process.env.DISABLE_TELEMETRY)
  7. Call chain tracing via string searchdata.count(b'Zv()'), data.find(b'!Zv()'), data.find(b'function u1_') to map all callers of Zv() and understand downstream impact
  8. GitHub source trace — @Tuxerino420's analysis in #29580 provided the original call chain (Zvq → vn → qA → Ar → Pv) from v2.1.69, confirmed structurally identical in v2.1.76

Gotchas & How to Address Them

  1. TWO COPIES of the JS bundle exist in the binary (Bun artifact). Both must be patched or the unpatched copy may be used at runtime. The script asserts exactly 2 matches for every pattern.

  2. L8T() has the SAME !! pattern as Zv(). A naive find-replace patches both. L8T() controls feedback survey suppression — patching it re-enables surveys. Fix: use context-aware matching with s_(FOUNDRY)|| prefix and }function L8T suffix as anchors.

  3. Patching Zv() alone re-enables analytics. u1_(){return!Zv()} returns true when Zv() returns false. This gates $6$() event sending. Fix: also patch u1_() to always return false.

  4. macOS kills unsigned binaries with SIGKILL (no error message, just "Killed: 9"). Must re-sign with codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime after any byte modification. The --preserve-metadata keeps entitlements (network access, hardened runtime) intact.

  5. Byte length must be identical. !!0& is 2 bytes each. Any length change shifts all subsequent bytes, corrupting the binary. The script asserts len(patched) == len(original).

  6. DISABLE_TELEMETRY=0 ALSO triggers the bug. In JavaScript, !!"0" is true because "0" is a non-empty string. Setting the env var to "0" or "false" does NOT disable it — the key must be completely REMOVED from settings.json. The 0& patch makes the value irrelevant.

  7. Auto-updates overwrite the patch. After claude update or automatic background updates, the binary is replaced. Re-run the script. The minified function names (Zv, u1_, L8T, s_) are version-specific and WILL change — the script aborts with a clear error if patterns don't match.

  8. Statsig gate exposure events cannot be fully suppressed without also disabling feature gates. This is the minimum server contact required — a read-only fetch of feature gate configurations.

  9. strings splits long JS lines unpredictably. Makes grep -B/-A context unreliable. Fix: use Python binary search (data.find()) with explicit byte windows for accurate context extraction.

  10. grep -P (Perl regex) unavailable on macOS. Use grep -Eo (extended regex) or Python for pattern matching.

Version-Specific Identifiers

v2.1.76 name Earlier name (v2.1.69) Purpose
Zv Pv Feature-gate bypass function (THE BUG)
s_ w1 Proper boolean env var parser
u1_ varies Analytics event gate
L8T varies Telemetry-only check (feedback surveys)
$6$ varies Custom event sender
Q varies Low-level Statsig event logger
BKK varies Statsig flush-on-exit gate

Caveats

  1. Auto-updates overwrite the patch. Claude Code auto-updates replace the binary. Re-run the script after each update. Function names may change across versions — the script will abort with a clear error if patterns aren't found.
  2. Statsig gate exposure events. Feature flag evaluation requires minimal Statsig server contact (downloading gate configs). Gate exposure events are inherent to this — they can't be disabled without also disabling the gates. Custom analytics ($6$) and feedback surveys remain fully disabled.
  3. Version-specific. The minified names (Zv, u1_, L8T, s_) are specific to v2.1.76. The script's pattern matching may need updating for future versions.
#!/usr/bin/env python3
"""
Patch Claude Code binary to fix DISABLE_TELEMETRY blocking feature gates.
Bug: DISABLE_TELEMETRY=1 in ~/.claude/settings.json disables ALL feature-gate
evaluation (GrowthBook/Statsig), not just telemetry. This blocks 1M context,
/remote-control, and all other gated features.
Root cause: Zv() uses !! (truthy check) instead of s_() (proper boolean parser)
for DISABLE_TELEMETRY and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC env vars.
Patches applied (2 targeted patches — env vars remain in control):
1. statsig_gate [Ai() v2.1.76 / hi() v2.1.77]:
Replace return analytics_gate() with return!1||!0 (always true — Statsig enabled).
Root cause fix: statsig_gate() coupled "is Statsig enabled?" to analytics_gate(),
which depends on telemetry_gate(), which depends on DISABLE_TELEMETRY. Setting
DISABLE_TELEMETRY=1 blocked ALL feature gates (1M context, /remote-control).
Patching statsig_gate() to always return true decouples Statsig from analytics.
2. extra_usage_check [bf7() v2.1.76 / Ij7() v2.1.77]:
Replace if(_===void 0)return!1 with if(_===void 0)return!0
(fixes race condition: cachedExtraUsageDisabledReason=undefined means "not yet loaded",
not "disabled" — optimistic allow until the API response arrives)
NOT patched (intentional — env vars continue to drive these):
- telemetry_gate [Zv/Cv]: Checks DISABLE_TELEMETRY and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC.
When set, telemetry_gate()=true → analytics_gate()=false (analytics off), BKK()=false (Segment blocked).
- analytics_gate [u1_/o1_]: Returns !telemetry_gate() — analytics off when DISABLE_TELEMETRY=1.
- Segment init [b27/varies]: Checks BKK() which inverts telemetry_gate() — Segment blocked when DISABLE_TELEMETRY=1.
- feedback_gate [L8T/v8T]: Feedback survey suppression stays active (uses !! which is correct here).
- $_(): Sentry error reporting stays disabled (uses DISABLE_ERROR_REPORTING).
Why only Ai() needs patching (not Zv/u1_/b27):
Original code chain when DISABLE_TELEMETRY=1:
Zv()=true → u1_()=!Zv()=false → analytics off ✓
→ BKK()=!Zv()=false → b27 skips → Segment blocked ✓
→ Ai()=u1_()=false → Oq() bails → ALL gates default false ✗ (BUG)
Patching Ai()=true breaks only the last link. Everything else is env-var-driven.
DATA COLLECTION FLAGS — all env-controlled after minimal 2-patch approach:
DISABLE_TELEMETRY / CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:
- Analytics events (u1_ → !Zv()): off when set, on when not set ✓
- Segment SDK (b27 → BKK() → !Zv()): blocked when set, works when not set ✓
- Feature gates (Ai() patch): ALWAYS enabled — Statsig decoupled from analytics ✓
DISABLE_ERROR_REPORTING → Sentry ($_) fully respected ✓
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY → survey suppression fully respected ✓
DISABLE_FEEDBACK_COMMAND → /feedback command gate fully respected ✓
DISABLE_BUG_COMMAND → /bug command gate fully respected ✓
DISABLE_AUTOUPDATER → auto-update gate (WO_()) NOT touched by patches ✓
DISABLE_COST_WARNINGS → cost warnings NOT touched by patches ✓
NOTE: DISABLE_NON_ESSENTIAL_MODEL_CALLS does NOT exist in v2.1.76 (0 occurrences).
NOTE: Auto-updates replace the binary and clear all patches. Re-run this script after
each update. The script aborts safely if function names changed (see Investigation
Methodology below for how to find new names in a different version).
Reference implementations (kept for documentation, NOT run by main()):
patch_zv() — was: replace !! with 0& for telemetry env vars in Zv() [NOT USED]
patch_u1_() — was: replace return!Zv() with return!1&&0 in u1_() [NOT USED]
patch_b27() — was: replace BKK() with (0&&0) in b27 Segment init [NOT USED]
These three hardcoded outcomes regardless of env vars and are superseded by the
minimal Ai()-only approach above.
Known limitations (inherent, NOT patchable):
- GrowthBook remoteEval to api.anthropic.com — REQUIRED for feature gate evaluation.
Sends user metadata (deviceId, sessionId, platform, orgUUID, accountUUID, userType,
subscriptionType, rateLimitTier) to the same API endpoint as conversations.
CANNOT be blocked without breaking feature gates entirely.
- hZ_() local cache writes — NOT network traffic, writes gate values to disk.
Required for gate persistence between sessions. NOT a privacy concern.
Run `analyze_claude_binary.py traffic-analysis` for full network traffic analysis.
References:
- https://github.com/anthropics/claude-code/issues/29580#issuecomment-4002014943
- https://github.com/anthropics/claude-code/issues/34083
- https://github.com/anthropics/claude-code/issues/34143
Default mode is dry-run. Use --real-run to apply changes.
Investigation Methods:
The Claude Code CLI is a Bun-compiled executable with the entire JS bundle stored
as plaintext inside the binary (not compressed). Platform-specific formats:
macOS: Mach-O 64-bit executable (arm64 or x86_64)
Linux: ELF 64-bit executable
Windows: PE32+ executable (claude.exe)
On macOS the minified JS lives in the __const section of the __TEXT segment;
on Linux/Windows it is embedded in a similar read-only data region.
Cross-platform investigation approach (works everywhere — Python only):
1. Python `data.find(b'!!process.env.DISABLE_TELEMETRY')` with a ±256-byte
context window — extracts surrounding JS to identify which function each
occurrence belongs to. Works on macOS, Linux, and Windows.
2. Python `data.count(b'DISABLE_TELEMETRY')` — counts total occurrences
(9 in v2.1.76 across both embedded JS copies, only 4 are in Zv/L8T).
3. Python `re.search(rb'function ([A-Za-z0-9_]+)[(][)][{].*?DISABLE_TELEMETRY', data)` —
pattern-based function name discovery; used by discover_function_names().
macOS/Linux-only investigation tools (for manual inspection):
4. `strings <binary> | grep 'DISABLE_TELEMETRY'` — confirms JS is plaintext,
finds all string references. (macOS: built-in; Linux: binutils package)
5. `grep -boa 'DISABLE_TELEMETRY' <binary>` — gets exact byte offsets of every
occurrence. (`-P` Perl regex unavailable on macOS grep; use `-Eo` or Python)
6. `otool -l <binary>` — dumps Mach-O load commands (macOS only).
Linux equivalent: `readelf -S <binary>` or `objdump -h <binary>`.
7. `file <binary>` — confirms binary type (available on macOS and Linux;
Windows: use `dumpbin /headers claude.exe` from MSVC tools or check PE
header with Python `data[:2] == b'MZ'`).
Gotchas & How to Address Them:
1. TWO COPIES of the JS bundle exist in the binary (Bun artifact). Both must be
patched or the unpatched copy may be used at runtime. The script asserts
exactly 2 matches for every pattern.
2. L8T() has the SAME `!!process.env.DISABLE_TELEMETRY` pattern as Zv(). A naive
find-replace would also patch L8T(), re-enabling feedback surveys. The script
uses context-aware matching: Zv()'s `!!` is preceded by `s_(FOUNDRY)||` and
the NONESSENTIAL `!!` is followed by `}function L8T` — these anchors ensure
only Zv() is patched.
3. Patching Zv() alone re-enables analytics via u1_(){return!Zv()}. When Zv()
returns false, u1_() returns true, enabling $6$() event sending. The u1_()
patch (return!1&&0) prevents this. However, Ai(){return u1_()} also calls
u1_(), and Oq() (feature gate query) checks if(!Ai())return T — so u1_()
returning false makes ALL gates default to false. The Ai() patch decouples
"Statsig enabled" from "analytics allowed" by making Ai() always return true.
4. macOS KILLS any binary with an invalid code signature (SIGKILL, no error msg).
After modifying bytes, the original Anthropic signature is invalid. Must re-sign
with `codesign --sign - --force --preserve-metadata=entitlements,flags,runtime`.
The --preserve-metadata keeps entitlements (network, hardened runtime) intact.
Linux and Windows do not use code signatures on the main executable — no
re-signing step is needed on those platforms.
5. `!!` vs `0&` must be EXACTLY the same byte length (2 bytes each). Any length
change shifts all subsequent bytes, corrupting the binary. The script asserts
len(patched) == len(original).
6. `DISABLE_TELEMETRY=0` ALSO triggers the bug because `!!"0"` is `true` in JS
("0" is a non-empty string). The env var must be completely REMOVED from
settings.json to fix without patching — setting it to "0" or "false" does not
work. The `0&` patch makes the value irrelevant.
7. Auto-updates overwrite the binary. After `claude update` or automatic background
updates, the patch is lost. Re-run this script. The minified function names
(Zv, u1_, L8T, s_) are version-specific and WILL change — the script aborts
with a clear error if patterns don't match.
8. Statsig gate exposure events cannot be fully suppressed without also disabling
feature gates. This is the minimum server contact required — a read-only fetch
of feature gate configurations.
9. The `strings` command (macOS/Linux) may split long JS lines across multiple
output lines, making grep context (-B/-A) unreliable. Use Python binary search
(data.find) with explicit byte windows for accurate context extraction.
On Windows, use Python directly — `strings` is not available by default.
10. `grep -P` (Perl regex) is unavailable on macOS default grep. Use `grep -Eo`
(extended regex) or Python for pattern matching on all platforms.
Version-specific identifiers (examples):
v2.1.76: telemetry_gate=Zv analytics_gate=u1_ statsig_gate=Ai extra_usage_check=bf7 state_accessor=DT feedback_gate=L8T
v2.1.77: telemetry_gate=Cv analytics_gate=o1_ statsig_gate=hi extra_usage_check=Ij7 state_accessor=wT feedback_gate=v8T
Earlier: Zv=Pv (e.g., v2.1.69 per @Tuxerino420's trace), s_=w1, names change every release.
Multi-version support:
- KNOWN_VERSIONS dict maps version strings to function name sets (fast, reliable path).
- discover_function_names() auto-discovers names from stable structural patterns for any
version not in KNOWN_VERSIONS. Stable patterns use unminified string literals:
telemetry_gate: function preceding '(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY'
analytics_gate: function XX(){return!telemetry_gate()}
statsig_gate: function XX(){return analytics_gate()} [PATCH TARGET 1]
extra_usage_check: function XX(){let _=SA().cachedExtraUsageDisabledReason;if(_===void 0)return [PATCH TARGET 2]
feedback_gate: function XX(){return!!process.env.DISABLE_TELEMETRY||!!...NONESSENTIAL...}
- If a future version changes code structure (not just names), update KNOWN_VERSIONS
manually and run: python3 analyze_claude_binary.py to investigate.
Investigation Methodology for New Versions:
When minified function names change (e.g., Zv→Pv, u1_→xx_), you must rediscover
them. The names change every version, but the STRUCTURE is stable. Follow these
steps to find the equivalent functions in any version.
The analysis script (analyze_claude_binary.py) can help, but its subcommands also
hardcode v2.1.76 names. The search/context subcommands work on any version.
Run `python3 analyze_claude_binary.py all` for a full investigation dump.
Step 1: Find the telemetry gate function (Zv equivalent)
─────────────────────────────────────────────────────────
Search for the env var string — it never changes between versions:
python3 analyze_claude_binary.py context '!!process.env.DISABLE_TELEMETRY' --window 300
This finds ALL functions containing `!!process.env.DISABLE_TELEMETRY`. There will be
TWO different functions (each appearing twice = 4 hits total):
a) The BUGGY function (Zv equivalent): contains BOTH `s_(process.env.CLAUDE_CODE_USE_FOUNDRY)`
AND `!!process.env.DISABLE_TELEMETRY`. It uses s_() for some vars but !! for others.
The function name is right after `function ` before `(){return s_(`.
b) The FEEDBACK function (L8T equivalent): contains ONLY `!!process.env.DISABLE_TELEMETRY`
without any s_() calls. This one should NOT be patched.
The distinguishing pattern: the buggy function has `s_(process.env.CLAUDE_CODE_USE_FOUNDRY)||`
immediately before the `!!process.env.DISABLE_TELEMETRY`. Use this as a context anchor.
Also search to understand s_() (the correct boolean parser):
python3 analyze_claude_binary.py context 'function s_(' --window 200
s_("1")=true, s_("true")=true, s_("")=false, s_("0")=false.
The bug is that !! treats ANY non-empty string as true, including "0" and "false".
Step 2: Find the analytics gate function (u1_ equivalent)
──────────────────────────────────────────────────────────
Search for the function that calls the Zv equivalent with a NOT:
python3 analyze_claude_binary.py context 'return!Zv_name_here()' --window 100
Or more generically, search for callers of the telemetry gate:
python3 analyze_claude_binary.py context '!Zv_name_here()' --window 100
The analytics gate is a short function: `function XX(){return!TELEMETRY_GATE()}`
It inverts the telemetry gate: when telemetry is "disabled", analytics is "enabled".
This function gates ALL analytics event callers (search for `!XX()` to find them):
- 1st-party event sender (Lh_ equivalent): `if(!XX())return;`
- Experiment tracking (Taq equivalent): `if(!XX())return;`
- Event batching (hw9/j6$ equivalents): check for XX() in their guards
Step 3: Find the Statsig-enabled function (Ai equivalent)
──────────────────────────────────────────────────────────
Search for any function that simply calls the analytics gate:
python3 analyze_claude_binary.py context 'return u1_name_here()' --window 100
It's a one-liner: `function YY(){return ANALYTICS_GATE()}`
Then find the feature gate query function (Oq equivalent) that checks it:
python3 analyze_claude_binary.py context '!YY()' --window 200
Look for the pattern: `if(!YY())return T` — this is the gate query defaulting to
false when Statsig is "disabled". This function is Oq(). Without patching Ai/YY,
ALL feature gates (1M context, /remote-control, etc.) default to false.
Step 4: Find the Segment SDK init (b27 equivalent)
───────────────────────────────────────────────────
Search for Segment write keys (these are stable across versions):
python3 analyze_claude_binary.py context 'LKJN8LsLERHEOXkw487o7qCTFOrGPimI' --window 300
Or search for the lazy init pattern with the nonessential traffic gate:
python3 analyze_claude_binary.py context 'await BKK_name_here())return null' --window 200
Or generically, search for the Segment SDK constructor:
python3 analyze_claude_binary.py context 'writeKey:' --window 200
The init function uses _q(async()=>{...}) lazy-init pattern and is gated by BKK/
nonessential-traffic check. The function name is `XXX=_q(async()=>{if(!await`.
Step 5: Find the nonessential traffic gate (BKK equivalent)
────────────────────────────────────────────────────────────
Search for the function that inverts the telemetry gate for traffic:
python3 analyze_claude_binary.py context 'if(Zv_name_here())return!1;return!0' --window 100
This is BKK(): returns true when telemetry is not disabled (traffic allowed).
After patching Zv, BKK returns true → Segment SDK init is gated by this.
Step 6: Verify the call chain
──────────────────────────────
Before patching, verify the dependency chain by searching for callers:
python3 analyze_claude_binary.py search 'function_name()'
Confirm:
- Telemetry gate (Zv) is called by: analytics gate (u1_), traffic gate (BKK), PKK init
- Analytics gate (u1_) is called by: Statsig gate (Ai), event callers (Lh_, Taq, etc.)
- Statsig gate (Ai) is called by: gate query (Oq), GrowthBook init (Kaq)
- Segment init (b27) is called by: event senders (ZLq, I27)
Step 7: GrowthBook / feature gate analysis
────────────────────────────────────────────
Search for stable GrowthBook patterns (client key, remoteEval):
python3 analyze_claude_binary.py context 'remoteEval:!0' --window 400
python3 analyze_claude_binary.py context 'sdk-zAZezfDKGoZuXXKe' --window 200
Search for user attributes sent to api.anthropic.com:
python3 analyze_claude_binary.py context 'deviceId' --window 200
Search for the local cache writer (NOT network):
python3 analyze_claude_binary.py context 'cachedGrowthBookFeatures' --window 300
Search for experiment exposure tracking:
python3 analyze_claude_binary.py context 'experimentId' --window 200
Step 8: Construct patches with byte-length safety
──────────────────────────────────────────────────
All patches MUST be exactly the same byte length as the original pattern.
Safe same-length replacements:
- `!!` → `0&` (2 bytes each: both falsy, but 0& ignores the value)
- `return!Zv()` → `return!1&&0` (12 bytes each: always false)
- `return u1_()` → `return!1||!0` (12 bytes each: always true)
- `if(!await BKK())return null` → `if(!await(0&&0))return null` (27 bytes each)
Use Python assert to verify: `assert len(original) == len(replacement)`
Use context anchors (surrounding unique text) to avoid patching wrong locations.
Step 9: Verify with analysis script
─────────────────────────────────────
After patching, verify ALL of these:
python3 analyze_claude_binary.py verify-patch # patch correctness
python3 analyze_claude_binary.py traffic-analysis # network traffic
python3 analyze_claude_binary.py patch-status # per-function status
Start a new Claude Code session and test:
/context → should show 1000k max (not 200k)
/model opus[1m] → should succeed
/help → should list /remote-control
Step 10: Env vars and feature gates to search for (stable across versions)
──────────────────────────────────────────────────────────────────────────
These env var names and gate names are stable (not minified):
python3 analyze_claude_binary.py search 'DISABLE_TELEMETRY'
python3 analyze_claude_binary.py search 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'
python3 analyze_claude_binary.py search 'DISABLE_ERROR_REPORTING'
python3 analyze_claude_binary.py search 'tengu_log_segment_events'
python3 analyze_claude_binary.py search 'tengu_log_datadog_events'
python3 analyze_claude_binary.py search 'tengu_1p_event_batch_config'
python3 analyze_claude_binary.py search 'cachedGrowthBookFeatures'
python3 analyze_claude_binary.py search 'cachedStatsigGates'
python3 analyze_claude_binary.py search 'analytics.segment.com'
python3 analyze_claude_binary.py search 'cdn.growthbook.io'
python3 analyze_claude_binary.py search 'remoteEval'
Step 11: Identifying the two JS bundle copies
──────────────────────────────────────────────
The Bun compiler embeds the JS bundle twice in the binary. Search for any
function definition and verify you get exactly 2 hits at very different offsets
(typically ~102MB apart). If you get 1 hit, the binary may be stripped. If you
get 3+, the binary structure has changed.
python3 analyze_claude_binary.py search 'function Zv_name_here()'
Both copies must be patched — the runtime may use either one.
"""
import argparse
import glob
import os
import re
import shutil
import subprocess
import sys
import time
# Known versions with their minified function names.
# Minified names change every release; the script auto-discovers names for
# unknown versions using stable structural patterns (see discover_function_names).
#
# Keys per version:
# telemetry_gate — Zv/Cv equiv: checks DISABLE_TELEMETRY env var
# analytics_gate — u1_/o1_ equiv: returns !telemetry_gate()
# statsig_gate — Ai/hi equiv: PATCH TARGET 1 (returns analytics_gate())
# extra_usage_check — bf7/Ij7 equiv: PATCH TARGET 2 (reads cachedExtraUsageDisabledReason)
# state_accessor — DT/wT equiv: accessor that returns cachedExtraUsageDisabledReason
# feedback_gate — L8T/v8T equiv: feedback survey suppression gate (must NOT be patched)
KNOWN_VERSIONS = {
"2.1.76": {
"telemetry_gate": "Zv", # Pv in earlier versions
"analytics_gate": "u1_",
"statsig_gate": "Ai", # PATCH TARGET 1
"extra_usage_check": "bf7", # PATCH TARGET 2
"state_accessor": "DT",
"feedback_gate": "L8T",
},
"2.1.77": {
"telemetry_gate": "Cv",
"analytics_gate": "o1_",
"statsig_gate": "hi", # PATCH TARGET 1
"extra_usage_check": "Ij7", # PATCH TARGET 2
"state_accessor": "wT",
"feedback_gate": "v8T",
},
}
# Number of JS bundle copies in the Bun-compiled Mach-O binary.
# Both copies must be patched or the unpatched copy may be used at runtime.
EXPECTED_COPY_COUNT = 2
def discover_function_names(data):
"""Auto-discover minified function names using stable structural patterns.
All patterns use unminified string literals or structural signatures that
do not change between versions (only the surrounding function names change).
Discovery steps (each builds on the prior):
1. telemetry_gate — function containing FOUNDRY + DISABLE_TELEMETRY
2. analytics_gate — function returning !telemetry_gate()
3. statsig_gate — function returning analytics_gate() [PATCH TARGET 1]
4. extra_usage_check + state_accessor — function reading cachedExtraUsageDisabledReason
5. feedback_gate — function returning !!DISABLE_TELEMETRY||!!NONESSENTIAL_TRAFFIC
Returns dict with discovered keys (some may be absent if pattern not found).
"""
names = {}
# Step 1: telemetry gate (Zv/Cv equiv)
# Only this function precedes (CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY
anchor = b'(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY'
pos = data.find(anchor)
if pos >= 0:
# 256-byte window: enough for the full return statement before the anchor
ctx = data[max(0, pos - 256):pos]
matches = list(re.finditer(rb'function ([A-Za-z_$][A-Za-z0-9_$]*)\(\)', ctx))
if matches:
names["telemetry_gate"] = matches[-1].group(1).decode()
else:
names["_warn_telemetry_gate"] = (
f"Could not find 'function XX()' in {min(pos, 256)} bytes before "
f"anchor '...CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY'. "
f"The function declaration may be further back. "
f"Add this version to KNOWN_VERSIONS manually."
)
elif pos == -1:
names["_warn_telemetry_gate"] = (
"FOUNDRY+DISABLE_TELEMETRY anchor not found in binary. "
"Binary structure may have changed significantly."
)
# Step 2: analytics gate (u1_/o1_ equiv)
# Stable signature: function XX(){return!TELEMETRY_GATE()}
tg = names.get("telemetry_gate")
if tg:
pattern_ag = rb'function ([A-Za-z_$][A-Za-z0-9_$]*)\(\)\{return!' + re.escape(tg.encode()) + rb'\(\)\}'
ag_all = re.findall(pattern_ag, data)
distinct_ag = list(dict.fromkeys(n.decode() for n in ag_all)) # deduplicate, preserve order
if len(distinct_ag) == 1:
names["analytics_gate"] = distinct_ag[0]
elif len(distinct_ag) > 1:
names["_warn_analytics_gate"] = (
f"Multiple distinct functions match analytics_gate pattern: {distinct_ag}. "
f"Expected exactly 1 distinct name ({EXPECTED_COPY_COUNT} copies). "
f"Add this version to KNOWN_VERSIONS manually."
)
# Step 3: statsig gate (Ai/hi equiv) — PATCH TARGET 1
# Stable signature: one-liner function returning exactly ANALYTICS_GATE()
ag = names.get("analytics_gate")
if ag:
pattern_sg = rb'function ([A-Za-z_$][A-Za-z0-9_$]*)\(\)\{return ' + re.escape(ag.encode()) + rb'\(\)\}'
sg_all = re.findall(pattern_sg, data)
distinct_sg = list(dict.fromkeys(n.decode() for n in sg_all))
if len(distinct_sg) == 1:
names["statsig_gate"] = distinct_sg[0]
elif len(distinct_sg) > 1:
names["_warn_statsig_gate"] = (
f"Multiple distinct functions match statsig_gate pattern: {distinct_sg}. "
f"Expected exactly 1 distinct name ({EXPECTED_COPY_COUNT} copies). "
f"Add this version to KNOWN_VERSIONS manually."
)
# Step 4: extra usage check + state accessor (bf7/Ij7 + DT/wT equiv) — PATCH TARGET 2
# Stable signature: contains cachedExtraUsageDisabledReason;if(_===void 0)return
m = re.search(
rb'function ([A-Za-z_$][A-Za-z0-9_$]*)\(\)\{let _='
rb'([A-Za-z_$][A-Za-z0-9_$]*)\(\)\.cachedExtraUsageDisabledReason;if\(_===void 0\)return',
data,
)
if m:
names["extra_usage_check"] = m.group(1).decode()
names["state_accessor"] = m.group(2).decode()
# Step 5: feedback gate (L8T/v8T equiv) — must NOT be patched
# Stable: exactly !!DISABLE_TELEMETRY||!!NONESSENTIAL_TRAFFIC (no FOUNDRY prefix)
m = re.search(
rb'function ([A-Za-z_$][A-Za-z0-9_$]*)\(\)\{'
rb'return!!process\.env\.DISABLE_TELEMETRY'
rb'\|\|!!process\.env\.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\}',
data,
)
if m:
names["feedback_gate"] = m.group(1).decode()
return names
def get_function_names(data, version_str):
"""Return function names to use for patching.
Priority:
1. If version matches a KNOWN_VERSIONS entry, use pre-mapped names (fast, reliable).
2. Otherwise auto-discover using stable structural patterns in the binary.
3. If discovery cannot find required names, return (None, missing_list).
Returns (names_dict, None) on success, (None, [missing_keys]) on failure.
"""
required = ["statsig_gate", "analytics_gate", "extra_usage_check", "state_accessor"]
# Fast path: known version lookup (exact match on the bare version number)
# Strip common prefixes like "v" or "Claude Code " so "2.1.77" matches
# "2.1.77 (Claude Code)" and "v2.1.77" but NOT "2.1.770".
version_bare = re.sub(r'^[^0-9]*', '', version_str or '').split()[0] if version_str else ''
for known_ver, known_names in KNOWN_VERSIONS.items():
if version_bare == known_ver:
print(f" Using known function names for v{known_ver}")
return known_names, None
# Slow path: auto-discover from binary
if version_str:
print(f" v{version_str} not in known versions ({', '.join(KNOWN_VERSIONS)}) — auto-discovering...")
else:
print(f" Unknown version — attempting auto-discovery...")
names = discover_function_names(data)
# Surface any discovery warnings before checking for missing keys
for key, val in names.items():
if key.startswith("_warn_"):
print(f" WARNING: {val}", file=sys.stderr)
missing = [k for k in required if not names.get(k)]
if missing:
return None, missing
print(f" Auto-discovered function names:")
for key in ["telemetry_gate", "analytics_gate", "statsig_gate",
"extra_usage_check", "state_accessor", "feedback_gate"]:
if key in names:
print(f" {key}: {names[key]}")
return names, None
def find_binary():
"""Find the Claude Code binary, searching platform-specific install locations.
Search order:
macOS / Linux:
1. ~/.local/bin/claude — official installer symlink
2. shutil.which("claude") — PATH (npm global, system package, etc.)
Windows:
1. %LOCALAPPDATA%\\AnthropicClaude\\app-*\\claude.exe — Squirrel installer
(newest app-X.Y.Z directory tried first)
2. %LOCALAPPDATA%\\AnthropicClaude\\claude.exe — flat install fallback
3. shutil.which("claude.exe") — PATH
4. shutil.which("claude") — PATH (no extension, e.g. WSL-style)
Symlinks are resolved to the real binary path (Bun bundles are in the real file,
not the symlink). Returns the real path. Raises SystemExit if nothing is found.
Use --binary /path/to/claude to override auto-detection.
"""
candidates = []
if sys.platform == "win32":
# Squirrel (Electron) installer places versioned directories under AnthropicClaude.
# LOCALAPPDATA is the canonical location; fall back to reconstructed path if unset.
local_app = os.environ.get("LOCALAPPDATA") or os.path.join(
os.path.expanduser("~"), "AppData", "Local"
)
app_glob = os.path.join(local_app, "AnthropicClaude", "app-*", "claude.exe")
# Newest version first (lexicographic desc == semantic desc for X.Y.Z format)
candidates.extend(sorted(glob.glob(app_glob), reverse=True))
candidates.append(os.path.join(local_app, "AnthropicClaude", "claude.exe"))
for name in ("claude.exe", "claude"):
found = shutil.which(name)
if found:
candidates.append(found)
else:
# macOS and Linux: official installer symlink at ~/.local/bin/claude
candidates.append(os.path.expanduser("~/.local/bin/claude"))
found = shutil.which("claude")
if found:
candidates.append(found)
# Walk candidates, resolve symlinks, return first real file found.
seen: set = set()
for candidate in candidates:
if not candidate or candidate in seen:
continue
seen.add(candidate)
if os.path.exists(candidate):
real = os.path.realpath(candidate)
if os.path.isfile(real):
return real
# Nothing found — platform-specific guidance.
if sys.platform == "win32":
locations = r"%LOCALAPPDATA%\AnthropicClaude\app-*\claude.exe or PATH"
else:
locations = "~/.local/bin/claude or PATH"
print(f"ERROR: Claude Code binary not found (searched: {locations}).", file=sys.stderr)
print(" Install Claude Code, or use --binary /path/to/claude.", file=sys.stderr)
sys.exit(1)
def check_version(binary_path):
"""Read and report the binary version.
Returns (known: bool, version_string: str).
'known' is True if the version is in KNOWN_VERSIONS.
Never aborts — get_function_names() handles unknown versions via auto-discovery.
"""
try:
result = subprocess.run(
[binary_path, "--version"],
capture_output=True, text=True, timeout=15,
)
version = result.stdout.strip() if result.returncode == 0 else ""
except (subprocess.TimeoutExpired, FileNotFoundError):
version = ""
version_bare = re.sub(r'^[^0-9]*', '', version).split()[0] if version else ''
known = version_bare in KNOWN_VERSIONS
if known:
print(f" Version: {version} (known — using pre-mapped function names)")
elif version:
print(f" Version: {version}")
print(f" Not in known versions ({', '.join(KNOWN_VERSIONS)}).")
print(f" Will auto-discover function names from binary structure.")
else:
print(f"WARNING: Could not determine binary version (--version returned empty)")
print(f" Known versions: {', '.join(KNOWN_VERSIONS)}. Will attempt auto-discovery.")
return known, version
def create_backup(path):
"""Create a timestamped backup of the binary.
Returns the backup path. Never overwrites existing backups.
"""
ts = time.strftime("%Y%m%d_%H%M%S")
bak = f"{path}.bak.{ts}"
if os.path.exists(bak):
print(f"ERROR: Backup already exists: {bak}", file=sys.stderr)
print(" Wait 1 second and retry, or specify a different --binary path.", file=sys.stderr)
sys.exit(1)
try:
shutil.copy2(path, bak)
except OSError as e:
print(f"ERROR: Failed to create backup at {bak}: {e}", file=sys.stderr)
print(" Check available disk space and write permissions.", file=sys.stderr)
sys.exit(1)
size_mb = os.path.getsize(bak) / (1024 * 1024)
print(f" Backup created: {bak} ({size_mb:.1f} MB)")
return bak
def find_oldest_backup(path):
"""Find the oldest .bak.* file by mtime — the true original unpatched binary.
Uses file modification time (not filename sort) so manually-created or
out-of-order backup filenames don't affect which is selected as the original.
Returns None if no backups exist.
"""
backups = glob.glob(f"{path}.bak.*")
return min(backups, key=os.path.getmtime) if backups else None
def find_all_backups(path):
"""List all backup files sorted oldest-first by mtime."""
backups = glob.glob(f"{path}.bak.*")
return sorted(backups, key=os.path.getmtime)
def ensure_backup(path):
"""Create backup only if no backup exists yet (preserve the true original).
The first backup IS the original binary. Subsequent patch runs should NOT
create new backups — they'd capture an already-patched or re-signed binary.
Returns the backup path (existing or newly created).
"""
existing = find_oldest_backup(path)
if existing:
size_mb = os.path.getsize(existing) / (1024 * 1024)
print(f" Original backup preserved: {existing} ({size_mb:.1f} MB)")
return existing
return create_backup(path)
def _superseded_patch_zv(data):
"""SUPERSEDED — DO NOT ADD TO MAIN PATCH LOOP.
Retained for documentation and rollback diagnostics only.
Why superseded: patching Zv() makes it always return 0, causing DISABLE_TELEMETRY
to no longer control analytics or Segment. This required also hardcoding u1_() and
b27() off, which removed env-var control over data collection entirely.
The minimal fix (patch_ai only) achieves working feature gates without touching
Zv/u1_/b27, leaving all data-collection flags fully respected.
Verify_no_collateral() will flag Zv() patterns as COLLATERAL DAMAGE if found.
Original purpose: Patch Zv(): replace !! with 0& for DISABLE_TELEMETRY and
NONESSENTIAL_TRAFFIC. Uses context-aware matching to only patch inside Zv(), not
L8T() or elsewhere. Each sub-patch is idempotent — skips if already applied.
Returns (patched_data, list_of_applied, list_of_skipped).
Returns (None, [], [error_messages]) on unexpected pattern counts.
"""
applied = []
skipped = []
# Patch 1: DISABLE_TELEMETRY !! in Zv()
# The unique context is the preceding s_(FOUNDRY) call — only Zv() has this
zv_tel_before = b's_(process.env.CLAUDE_CODE_USE_FOUNDRY)||!!process.env.DISABLE_TELEMETRY'
zv_tel_after = b's_(process.env.CLAUDE_CODE_USE_FOUNDRY)||0&process.env.DISABLE_TELEMETRY'
already = data.count(zv_tel_after)
if already >= EXPECTED_COPY_COUNT:
skipped.append(f"Zv() DISABLE_TELEMETRY: already patched ({already} copies)")
else:
count = data.count(zv_tel_before)
if count == 0:
return None, [], [
f"FAIL: Zv() DISABLE_TELEMETRY pattern not found (0 copies).",
f" Expected pattern: {zv_tel_before[:60]}...",
f" Binary may be a different version — update KNOWN_VERSIONS or run auto-discovery.",
f" Run: python3 analyze_claude_binary.py patch-status",
]
if count != EXPECTED_COPY_COUNT:
return None, [], [
f"FAIL: Expected {EXPECTED_COPY_COUNT} copies of Zv() DISABLE_TELEMETRY, found {count}.",
f" Bun binaries contain {EXPECTED_COPY_COUNT} JS bundle copies; unexpected count suggests corruption or version change.",
]
data = data.replace(zv_tel_before, zv_tel_after)
applied.append(f"Zv() DISABLE_TELEMETRY: replaced !! with 0& ({count} copies)")
# Patch 2: NONESSENTIAL_TRAFFIC !! in Zv()
# Use }function L8T as suffix anchor to only match the Zv() occurrence
zv_nt_before = b'||!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}function L8T'
zv_nt_after = b'||0&process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}function L8T'
already = data.count(zv_nt_after)
if already >= EXPECTED_COPY_COUNT:
skipped.append(f"Zv() NONESSENTIAL_TRAFFIC: already patched ({already} copies)")
else:
count = data.count(zv_nt_before)
if count == 0:
return None, [], [
f"FAIL: Zv() NONESSENTIAL_TRAFFIC pattern not found (0 copies).",
f" Expected pattern: {zv_nt_before[:60]}...",
f" Binary may be a different version — update KNOWN_VERSIONS or run auto-discovery.",
]
if count != EXPECTED_COPY_COUNT:
return None, [], [
f"FAIL: Expected {EXPECTED_COPY_COUNT} copies of Zv() NONESSENTIAL, found {count}.",
]
data = data.replace(zv_nt_before, zv_nt_after)
applied.append(f"Zv() NONESSENTIAL_TRAFFIC: replaced !! with 0& ({count} copies)")
return data, applied, skipped
def _superseded_patch_u1(data):
"""SUPERSEDED — DO NOT ADD TO MAIN PATCH LOOP.
Retained for documentation and rollback diagnostics only.
Why superseded: hardcodes u1_() to always return false (analytics always off),
removing DISABLE_TELEMETRY's ability to control analytics. With the minimal
patch_ai approach, u1_() is left intact: it continues to return !Zv(), which
correctly returns false when DISABLE_TELEMETRY=1 and true when not set.
Verify_no_collateral() will flag u1_() hardcoded pattern as COLLATERAL DAMAGE.
Original purpose: Patch u1_(): replace return!Zv() with return!1&&0 to prevent
re-enabling analytics event sending after _superseded_patch_zv() is applied.
Idempotent — skips if already applied.
Returns (patched_data, list_of_applied, list_of_skipped).
"""
marker = b'function u1_(){return!Zv()}'
replacement = b'function u1_(){return!1&&0}'
assert len(marker) == len(replacement), "u1_ patch length mismatch"
already = data.count(replacement)
if already >= EXPECTED_COPY_COUNT:
return data, [], [f"u1_(): already patched ({already} copies)"]
count = data.count(marker)
if count == 0:
return None, [], [
f"FAIL: u1_() pattern not found (0 copies).",
f" Expected: {marker.decode()}",
f" Binary may be a different version — update KNOWN_VERSIONS or run auto-discovery.",
f" Run: python3 analyze_claude_binary.py functions",
]
if count != EXPECTED_COPY_COUNT:
return None, [], [
f"FAIL: Expected {EXPECTED_COPY_COUNT} copies of u1_() pattern, found {count}.",
]
data = data.replace(marker, replacement)
return data, [f"u1_(): replaced return!Zv() with return!1&&0 ({count} copies)"], []
def patch_statsig_gate(data, names):
"""Patch statsig gate: replace return ANALYTICS_GATE() with return!1||!0.
This function (Ai() in v2.1.76, hi() in v2.1.77) is called by Oq() as a guard:
if(!STATSIG_GATE())return default. It originally returns ANALYTICS_GATE(), which
returns !TELEMETRY_GATE(). When DISABLE_TELEMETRY=1, TELEMETRY_GATE()=true →
ANALYTICS_GATE()=false → STATSIG_GATE()=false → Oq() bails → ALL feature gates
return their default value (false). This blocks 1M context, /remote-control, etc.
The fix: STATSIG_GATE() always returns true, decoupling Statsig from analytics.
Telemetry gate, analytics gate, Segment (b27), and feedback gate remain intact.
!1||!0 = false||true = true. Same byte length as 'return XX_()' when the
analytics gate name is exactly 3 characters (e.g., u1_, o1_). The script verifies
the byte lengths match and aborts with a clear message if they don't.
Idempotent — skips if already applied.
Returns (patched_data, list_of_applied, list_of_skipped).
Returns (None, [], [errors]) on failure.
"""
fn = names["statsig_gate"]
ag = names["analytics_gate"]
replacement_inner = b"return!1||!0"
original_inner = f"return {ag}()".encode()
if len(original_inner) != len(replacement_inner):
# Build a same-length always-true replacement for the error hint.
# 'return!1||!0' is 12 bytes; 'return!0' is 8 bytes.
# We need len("return {ag}()") bytes that evaluate to true.
needed = len(original_inner)
# Pad "return!0" with a no-op expression to reach `needed` bytes.
# e.g., needed=14 → "return!0||!1&&0" (15)... use comment-style:
# "return(0||!0)" = 13, "return!!(!0)" = 12, etc.
# Most reliable: tell the user the exact length they need.
hint = (
f"Need a {needed}-byte always-true JS expression. "
f"Candidates: "
f"'return!1||!0' ({len(b'return!1||!0')}B), "
f"'return!(!0)' ({len(b'return!(!0)')}B), "
f"'return(0,!0)' ({len(b'return(0,!0)')}B), "
f"'return void 0||!0' ({len(b'return void 0||!0')}B). "
f"Add the matching expression to patch_statsig_gate() in KNOWN_VERSIONS "
f"as a per-version override, or open an issue."
)
return None, [], [
f"FAIL: Analytics gate name '{ag}' has length {len(ag)} "
f"(expected 3 chars for the default 'return!1||!0' replacement). "
f"'return {ag}()' is {needed} bytes but 'return!1||!0' is "
f"{len(replacement_inner)} bytes — lengths must match exactly. "
f"{hint}",
]
marker = f"function {fn}(){{return {ag}()}}".encode()
replacement = f"function {fn}(){{return!1||!0}}".encode()
assert len(marker) == len(replacement), "statsig_gate patch length mismatch"
already = data.count(replacement)
if already >= EXPECTED_COPY_COUNT:
return data, [], [f"{fn}() (statsig_gate): already patched ({already} copies)"]
count = data.count(marker)
if count == 0:
return None, [], [
f"FAIL: {fn}() (statsig_gate) pattern not found (0 copies).",
f" Expected: {marker.decode()}",
f" Binary version may not match discovered function names.",
f" Run: python3 analyze_claude_binary.py functions",
]
if count != EXPECTED_COPY_COUNT:
partial = f" (plus {already} copy already patched — binary is partially patched)" if already > 0 else ""
return None, [], [
f"FAIL: Expected {EXPECTED_COPY_COUNT} copies of {fn}() (statsig_gate) pattern, "
f"found {count}{partial}. "
f"If partially patched: rollback with --rollback --real-run, then re-apply.",
]
data = data.replace(marker, replacement)
return data, [
f"{fn}() (statsig_gate): replaced return {ag}() with return!1||!0 ({count} copies)"
], []
def patch_ai(data):
"""Backwards-compatible wrapper — calls patch_statsig_gate with v2.1.76 names."""
return patch_statsig_gate(data, KNOWN_VERSIONS["2.1.76"])
def _superseded_patch_b27(data):
"""SUPERSEDED — DO NOT ADD TO MAIN PATCH LOOP.
Retained for documentation and rollback diagnostics only.
Why superseded: hardcodes b27 to always skip Segment SDK init, removing
DISABLE_TELEMETRY's ability to control Segment. This was only needed because
_superseded_patch_zv() made BKK() always return true (Segment always allowed).
With the minimal patch_ai approach, Zv() is intact so BKK() correctly returns
false when DISABLE_TELEMETRY=1, keeping Segment blocked via the original path.
Verify_no_collateral() will flag b27() hardcoded pattern as COLLATERAL DAMAGE.
Original purpose: Patch b27 to block Segment SDK initialization unconditionally.
b27 is gated by BKK() which inverts Zv(). After _superseded_patch_zv(), BKK()
always returns true so Segment would initialize even with DISABLE_TELEMETRY=1.
Patch: if(!await BKK())return null → if(!await(0&&0))return null (always null).
Same byte length (44 bytes). Callers ZLq()/I27() bail on null return. SAFE.
Idempotent — skips if already applied.
Returns (patched_data, list_of_applied, list_of_skipped).
"""
# Use full context anchor to avoid matching other BKK() calls
ctx_marker = b"b27=_q(async()=>{if(!await BKK())return null"
ctx_replacement = b"b27=_q(async()=>{if(!await(0&&0))return null"
assert len(ctx_marker) == len(ctx_replacement), (
f"b27 patch length mismatch: original={len(ctx_marker)}, "
f"replacement={len(ctx_replacement)}"
)
already = data.count(ctx_replacement)
if already >= EXPECTED_COPY_COUNT:
return data, [], [f"b27(): already patched ({already} copies)"]
count = data.count(ctx_marker)
if count == 0:
return None, [], [
f"FAIL: b27() pattern not found (0 copies).",
f" Expected: {ctx_marker.decode()}",
f" Binary may be a different version — update KNOWN_VERSIONS or run auto-discovery.",
f" Run: python3 analyze_claude_binary.py segment",
]
if count != EXPECTED_COPY_COUNT:
return None, [], [
f"FAIL: Expected {EXPECTED_COPY_COUNT} copies of b27() pattern, found {count}.",
f" Bun binaries contain {EXPECTED_COPY_COUNT} JS bundle copies.",
]
data = data.replace(ctx_marker, ctx_replacement)
return data, [f"b27(): blocked Segment SDK init ({count} copies)"], []
def patch_extra_usage_check(data, names):
"""Patch extra usage check: replace if(_===void 0)return!1 with return!0.
This function (bf7() in v2.1.76, Ij7() in v2.1.77) is called by eF() and _Q()
to check whether extra-usage billing is enabled. It reads
STATE_ACCESSOR().cachedExtraUsageDisabledReason:
- undefined → API response not yet arrived (race condition on session start/resume)
- null → no reason to disable → extra usage allowed
- string → specific disable reason (overage_not_provisioned, out_of_credits, etc.)
The bug: undefined is treated as "disabled" (returns !1 = false). On session
start/resume, before the API response arrives, eF()/_Q() return false →
DaK()/faK() block 1M context.
The fix: undefined → !0 (true, optimistic allow). Real disabling reasons are still
handled by the switch statement below.
Scope: Only affects Pro/Team plan users where s8()=true (extra-usage billing applies).
For Max/Enterprise where s8()=false, eF()/_Q() return !0 directly without calling
this function — this patch is a no-op for those plans.
Same byte length: 'return!1' and 'return!0' are both 8 bytes. ✓ Always matches
regardless of function name or state accessor name length.
Idempotent — skips if already applied.
Returns (patched_data, list_of_applied, list_of_skipped).
Returns (None, [], [errors]) on failure.
"""
fn = names["extra_usage_check"]
sa = names["state_accessor"]
# Use the full function header as anchor to avoid any false matches
marker = (
f"function {fn}(){{let _={sa}().cachedExtraUsageDisabledReason;"
f"if(_===void 0)return!1;"
).encode()
replacement = (
f"function {fn}(){{let _={sa}().cachedExtraUsageDisabledReason;"
f"if(_===void 0)return!0;"
).encode()
assert len(marker) == len(replacement), "extra_usage_check patch length mismatch"
already = data.count(replacement)
if already >= EXPECTED_COPY_COUNT:
return data, [], [f"{fn}() (extra_usage_check): already patched ({already} copies)"]
count = data.count(marker)
if count == 0:
return None, [], [
f"FAIL: {fn}() (extra_usage_check) pattern not found (0 copies).",
f" Expected: {marker.decode()}",
f" Binary version may not match discovered function names.",
f" Run: python3 analyze_claude_binary.py context 'function {fn}'",
]
if count != EXPECTED_COPY_COUNT:
partial = f" (plus {already} copy already patched — binary is partially patched)" if already > 0 else ""
return None, [], [
f"FAIL: Expected {EXPECTED_COPY_COUNT} copies of {fn}() (extra_usage_check) pattern, "
f"found {count}{partial}. Bun binaries contain {EXPECTED_COPY_COUNT} JS bundle copies. "
f"If partially patched: rollback with --rollback --real-run, then re-apply.",
]
data = data.replace(marker, replacement)
return data, [
f"{fn}() (extra_usage_check): fixed race condition — undefined "
f"cachedExtraUsageDisabledReason now returns !0 (optimistic allow) "
f"instead of !1 ({count} copies)"
], []
def patch_bf7(data):
"""Backwards-compatible wrapper — calls patch_extra_usage_check with v2.1.76 names."""
return patch_extra_usage_check(data, KNOWN_VERSIONS["2.1.76"])
def verify_no_collateral(data, names):
"""Verify patches are correct and no unintended changes occurred.
Checks 6 invariants for the minimal 2-patch approach (statsig_gate + extra_usage_check):
1. Telemetry gate INTACT: env vars (DISABLE_TELEMETRY, NONESSENTIAL_TRAFFIC) still respected
2. Analytics gate INTACT: analytics follow env var via !telemetry_gate()
3. Statsig gate PATCHED: feature gate evaluation always enabled
4. Segment init INTACT: follows env var via BKK() → !telemetry_gate() (structural check)
5. Feedback gate INTACT: surveys stay suppressed when DISABLE_TELEMETRY set (if name known)
6. Extra usage check PATCHED: undefined cachedExtraUsageDisabledReason treated as allowed
Returns list of issue descriptions (empty = all OK).
"""
issues = []
sg = names["statsig_gate"]
ag = names["analytics_gate"]
fn = names["extra_usage_check"]
sa = names["state_accessor"]
fb = names.get("feedback_gate") # optional — only checked if discovered
# 1. Telemetry gate — must be INTACT (superseded 0& patch must not be present)
# The structural anchor (FOUNDRY prefix) is stable regardless of boolean parser name
superseded_tg = b'(process.env.CLAUDE_CODE_USE_FOUNDRY)||0&process.env.DISABLE_TELEMETRY'
count_0amp = data.count(superseded_tg)
if count_0amp > 0:
tg = names.get("telemetry_gate", "telemetry_gate")
issues.append(
f"COLLATERAL DAMAGE: {tg}() (telemetry_gate) has {count_0amp} copies with "
f"0& for DISABLE_TELEMETRY. This hardcodes behavior regardless of env var. "
f"Rollback with --rollback --real-run and re-apply."
)
superseded_nt = b'||0&process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'
count_nt = data.count(superseded_nt)
if count_nt > 0:
tg = names.get("telemetry_gate", "telemetry_gate")
issues.append(
f"COLLATERAL DAMAGE: {tg}() has {count_nt} copies with 0& for "
f"NONESSENTIAL_TRAFFIC. Rollback and re-apply."
)
# 2. Analytics gate — must be INTACT (superseded hardcoded-false pattern not present)
ag_hardcoded = f"function {ag}(){{return!1&&0}}".encode()
count_ag = data.count(ag_hardcoded)
if count_ag > 0:
issues.append(
f"COLLATERAL DAMAGE: {ag}() (analytics_gate) has {count_ag} hardcoded copies "
f"(return!1&&0). Analytics will be always-off regardless of DISABLE_TELEMETRY. "
f"Rollback with --rollback --real-run and re-apply."
)
# 3. Statsig gate — must be PATCHED
sg_still_old = data.count(f"function {sg}(){{return {ag}()}}".encode())
if sg_still_old > 0:
issues.append(
f"INCOMPLETE: {sg}() (statsig_gate) still has {sg_still_old} unpatched copies "
f"calling {ag}(). All feature gates will default to false (1M/remote-control "
f"blocked) when DISABLE_TELEMETRY=1."
)
sg_patched = data.count(f"function {sg}(){{return!1||!0}}".encode())
if sg_patched != EXPECTED_COPY_COUNT:
issues.append(
f"MISSING: {sg}() (statsig_gate) patch found {sg_patched} times "
f"(expected {EXPECTED_COPY_COUNT}). Feature gate evaluation may not be enabled."
)
# 4. Segment init — must be INTACT (structural check; Segment lazy-init variable name varies)
seg_hardcoded = b"=_q(async()=>{if(!await(0&&0))return null"
count_seg = data.count(seg_hardcoded)
if count_seg > 0:
issues.append(
f"COLLATERAL DAMAGE: Segment SDK init has {count_seg} hardcoded (0&&0) copies. "
f"Segment will be always-blocked regardless of DISABLE_TELEMETRY. "
f"Rollback with --rollback --real-run and re-apply."
)
# 5. Feedback gate — must be INTACT
if fb:
fb_pattern = (
f"function {fb}(){{return!!process.env.DISABLE_TELEMETRY"
f"||!!process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}}"
).encode()
count_fb = data.count(fb_pattern)
if count_fb == 0:
issues.append(
f"COLLATERAL DAMAGE: {fb}() (feedback_gate) not found (0 of "
f"{EXPECTED_COPY_COUNT} expected copies). The feedback gate function "
f"may have been overwritten. Rollback with --rollback --real-run and re-apply."
)
elif count_fb != EXPECTED_COPY_COUNT:
issues.append(
f"COLLATERAL DAMAGE: {fb}() (feedback_gate) found {count_fb} times "
f"(expected {EXPECTED_COPY_COUNT} unpatched copies). Binary structure "
f"is unexpected. Rollback with --rollback --real-run."
)
else:
# feedback_gate name was not discovered — emit a warning so the skip is visible
issues.append(
f"WARNING: feedback_gate name not discovered — check 5 (feedback gate integrity) "
f"was skipped. This is non-fatal but means the feedback survey gate was not "
f"verified. Add this version to KNOWN_VERSIONS to enable this check."
)
# 6. Extra usage check — race condition fixed (undefined → optimistic allow)
eu_still_old = data.count(
f"function {fn}(){{let _={sa}().cachedExtraUsageDisabledReason;"
f"if(_===void 0)return!1;".encode()
)
eu_patched = data.count(
f"function {fn}(){{let _={sa}().cachedExtraUsageDisabledReason;"
f"if(_===void 0)return!0;".encode()
)
if eu_still_old > 0:
issues.append(
f"INCOMPLETE: {fn}() (extra_usage_check) still has {eu_still_old} unpatched copies "
f"(return!1 for undefined). 1M context may be intermittently blocked on "
f"session start/resume (race condition: cachedExtraUsageDisabledReason undefined "
f"before API loads)."
)
if eu_patched != EXPECTED_COPY_COUNT:
issues.append(
f"MISSING: {fn}() (extra_usage_check) patch found {eu_patched} times "
f"(expected {EXPECTED_COPY_COUNT}). Race condition fix may not be active."
)
return issues
def resign_binary(path):
"""Ad-hoc codesign the binary for macOS.
Required after any binary modification on macOS, otherwise the OS
will refuse to execute it (SIGKILL with no error message).
Idempotent — re-signing an already ad-hoc-signed binary is safe.
On non-macOS platforms, codesigning is not required; returns True immediately.
"""
if sys.platform != "darwin":
print(" Skipping codesign (not macOS — not required)")
return True
# Attempt 1: preserve entitlements + flags + runtime hardening (omit 'requirements'
# which is incompatible with ad-hoc signing on macOS 14/15 in some configurations).
# Attempt 2: minimal ad-hoc sign (no --preserve-metadata) as last resort.
attempts = [
["--preserve-metadata=entitlements,flags,runtime"],
[], # minimal: no --preserve-metadata
]
last_stderr = ""
for extra_flags in attempts:
result = subprocess.run(
["codesign", "--sign", "-", "--force"] + extra_flags + [path],
capture_output=True,
text=True,
)
if result.returncode == 0:
desc = extra_flags[0] if extra_flags else "minimal (no --preserve-metadata)"
print(f" Binary re-signed successfully ({desc})")
return True
last_stderr = result.stderr.strip()
print(f" ERROR: codesign failed: {last_stderr}", file=sys.stderr)
print(" The binary will not run on macOS without a valid signature.", file=sys.stderr)
print(" Restore from backup: python3 patch_claude_binary.py --rollback --real-run", file=sys.stderr)
return False
def verify_binary(path):
"""Run claude --version to verify the patched binary works.
Returns True if the binary runs and produces version output.
"""
try:
result = subprocess.run(
[path, "--version"],
capture_output=True,
text=True,
timeout=15,
)
except subprocess.TimeoutExpired:
print(" ERROR: claude --version timed out after 15s", file=sys.stderr)
print(" The binary may be stuck. Restore from backup if needed.", file=sys.stderr)
return False
except FileNotFoundError:
print(f" ERROR: Binary not found at {path}", file=sys.stderr)
return False
if result.returncode != 0:
print(f" ERROR: claude --version failed (exit {result.returncode}): {result.stderr.strip()}", file=sys.stderr)
return False
version = result.stdout.strip()
print(f" Binary verified: {version}")
return True
def do_rollback(binary_path):
"""Restore the oldest backup (true original binary).
Always uses the oldest backup because:
- The first backup IS the original binary before any patches
- Later backups may contain intermediate patch states
"""
bak = find_oldest_backup(binary_path)
if not bak:
print(f"ERROR: No backup found matching {binary_path}.bak.*", file=sys.stderr)
print(" No rollback possible — no backup was ever created.", file=sys.stderr)
sys.exit(1)
all_backups = find_all_backups(binary_path)
if len(all_backups) > 1:
print(f" {len(all_backups)} backups found. Using oldest (true original):")
for b in all_backups:
marker = " ← restoring" if b == bak else ""
size_mb = os.path.getsize(b) / (1024 * 1024)
print(f" {b} ({size_mb:.1f} MB){marker}")
print(f"\nRolling back to: {bak}")
# Size comparison for sanity
orig_size = os.path.getsize(binary_path)
bak_size = os.path.getsize(bak)
size_diff = abs(orig_size - bak_size)
if size_diff > 1024 * 1024: # >1MB difference is suspicious
print(f" WARNING: Large size difference — current {orig_size:,} bytes, backup {bak_size:,} bytes")
print(f" This may indicate the backup is from a different version.")
try:
shutil.copy2(bak, binary_path)
except OSError as e:
print(f"ERROR: Failed to restore backup: {e}", file=sys.stderr)
print(f" Source: {bak}", file=sys.stderr)
print(f" Destination: {binary_path}", file=sys.stderr)
print(" Check available disk space and write permissions.", file=sys.stderr)
sys.exit(1)
print(f" Restored {binary_path} from backup")
if not resign_binary(binary_path):
print(f"\nERROR: codesign failed after rollback. Binary at {binary_path} may not run.", file=sys.stderr)
print(f" The restored file is {os.path.getsize(binary_path):,} bytes (from backup {bak}).", file=sys.stderr)
sys.exit(1)
if not verify_binary(binary_path):
print("\nWARNING: Restored binary failed verification.", file=sys.stderr)
print(" The backup may be from a different version than what's currently installed.", file=sys.stderr)
print(f" Available backups (use --rollback-list to inspect):", file=sys.stderr)
for b in all_backups:
size_mb = os.path.getsize(b) / (1024 * 1024)
print(f" {b} ({size_mb:.1f} MB)", file=sys.stderr)
return
print("\nRollback complete. Binary restored to unpatched state.")
def do_rollback_list(binary_path):
"""Show all available backups with metadata."""
backups = find_all_backups(binary_path)
if not backups:
print("No backups found.")
print(f" Expected pattern: {binary_path}.bak.<timestamp>")
return
print(f"=== Available Backups ({len(backups)}) ===\n")
for i, bak in enumerate(backups):
size_mb = os.path.getsize(bak) / (1024 * 1024)
label = "OLDEST (true original)" if i == 0 else f"backup #{i + 1}"
print(f" {bak}")
print(f" Size: {size_mb:.1f} MB | {label}")
print(f"\n Rollback uses: {backups[0]} (oldest = true original)")
print(f" To rollback: python3 {__file__} --rollback --real-run")
def main():
parser = argparse.ArgumentParser(
description="Patch Claude Code binary to fix DISABLE_TELEMETRY blocking feature gates.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Default mode is dry-run. Use --real-run to apply changes.
Backup:
On first --real-run, a backup is created next to the binary:
<binary-path>.bak.<timestamp> (e.g., .bak.20260315_190241)
Subsequent runs reuse the existing backup (no accumulation).
Use --rollback-list to see all backups.
Use --rollback --real-run to restore the oldest backup (true original).
""",
)
parser.add_argument(
"--real-run",
action="store_true",
help="Actually apply patches (default is dry-run)",
)
parser.add_argument(
"--rollback",
action="store_true",
help="Restore the oldest backup (true original binary)",
)
parser.add_argument(
"--rollback-list",
action="store_true",
help="List all available backups with metadata",
)
parser.add_argument(
"--binary",
type=str,
default=None,
help=(
"Path to Claude binary (default: auto-detect). "
"macOS/Linux: ~/.local/bin/claude or PATH. "
r"Windows: %%LOCALAPPDATA%%\AnthropicClaude\app-*\claude.exe or PATH."
),
)
args = parser.parse_args()
dry_run = not args.real_run
# Find binary
if args.binary:
binary_path = os.path.realpath(args.binary)
if not os.path.isfile(binary_path):
print(f"ERROR: File not found: {binary_path}", file=sys.stderr)
sys.exit(1)
else:
binary_path = find_binary()
print(f"Claude binary: {binary_path}")
print(f"Size: {os.path.getsize(binary_path) / (1024 * 1024):.1f} MB")
print()
# Handle --rollback-list
if args.rollback_list:
do_rollback_list(binary_path)
return
# Handle rollback
if args.rollback:
if dry_run:
bak = find_oldest_backup(binary_path)
if bak:
size_mb = os.path.getsize(bak) / (1024 * 1024)
print(f"[DRY RUN] Would restore from: {bak} ({size_mb:.1f} MB)")
all_backups = find_all_backups(binary_path)
if len(all_backups) > 1:
print(f" ({len(all_backups)} backups exist; rollback uses oldest = true original)")
else:
print(f"[DRY RUN] No backup found matching {binary_path}.bak.*")
return
do_rollback(binary_path)
return
# Version check (informational — auto-discovery handles unknown versions)
_, version = check_version(binary_path)
# Read binary
print("Reading binary...")
with open(binary_path, "rb") as f:
original_data = f.read()
# Resolve function names (known lookup or auto-discovery from binary structure)
print("\nResolving function names...")
names, missing = get_function_names(original_data, version)
if names is None:
print(f"ERROR: Could not resolve function names — missing: {', '.join(missing)}", file=sys.stderr)
print(f" This version is not in KNOWN_VERSIONS and auto-discovery failed.", file=sys.stderr)
print(f" Run analyze_claude_binary.py to investigate and add this version", file=sys.stderr)
print(f" to the KNOWN_VERSIONS dict at the top of this script.", file=sys.stderr)
sys.exit(1)
# Apply patches (each is idempotent — skips if already applied)
print("\nApplying patches...")
data = original_data
all_applied = []
all_skipped = []
for patch_fn in [
lambda d: patch_statsig_gate(d, names),
lambda d: patch_extra_usage_check(d, names),
]:
result_data, applied, skipped = patch_fn(data)
if result_data is None:
for msg in skipped:
print(f" ERROR: {msg}", file=sys.stderr)
sys.exit(1)
data = result_data
all_applied.extend(applied)
all_skipped.extend(skipped)
# Report
for msg in all_applied:
print(f" APPLIED: {msg}")
for msg in all_skipped:
print(f" SKIPPED: {msg}")
if not all_applied:
print("\nAll patches already applied. Nothing to do.")
print("Use --rollback --real-run to restore the original binary.")
return
# Verify no collateral damage
print("\nVerifying patch integrity...")
issues = verify_no_collateral(data, names)
warnings = [i for i in issues if i.startswith("WARNING:")]
errors = [i for i in issues if not i.startswith("WARNING:")]
for w in warnings:
print(f" {w}")
if errors:
for e in errors:
print(f" ERROR: {e}", file=sys.stderr)
print("\nAborting — patches would cause unintended changes.", file=sys.stderr)
sys.exit(1)
sg = names["statsig_gate"]
fn = names["extra_usage_check"]
print(f" All checks passed — {sg}()/{fn}() patched, telemetry/analytics/Segment/feedback gates intact")
# Verify byte length unchanged
if len(data) != len(original_data):
print(f"\nERROR: Binary size changed ({len(original_data):,}{len(data):,} bytes)", file=sys.stderr)
print("This should never happen with same-length replacements.", file=sys.stderr)
sys.exit(1)
print(f" Binary size unchanged: {len(data):,} bytes")
# Dry run stops here
if dry_run:
print(f"\n{'='*60}")
print("[DRY RUN] No changes written.")
print(f"Run with --real-run to apply these {len(all_applied)} patches.")
if all_skipped:
print(f"({len(all_skipped)} patches already applied, will be skipped.)")
print(f"{'='*60}")
return
# Real run: backup, write, resign, verify
print("\nEnsuring backup...")
bak_path = ensure_backup(binary_path)
def _restore_backup(reason):
"""Emergency restore helper — called when post-write steps fail."""
print(f"\nERROR: {reason}. Restoring from backup...", file=sys.stderr)
try:
shutil.copy2(bak_path, binary_path)
except OSError as e:
print(f" CRITICAL: Could not restore backup: {e}", file=sys.stderr)
print(f" Manual restore: cp '{bak_path}' '{binary_path}'", file=sys.stderr)
sys.exit(1)
print(f" Restored original from {bak_path}")
print("\nWriting patched binary...")
try:
with open(binary_path, "wb") as f:
f.write(data)
except OSError as e:
print(f"ERROR: Failed to write patched binary: {e}", file=sys.stderr)
print(" Check available disk space and write permissions.", file=sys.stderr)
sys.exit(1)
print(f" Wrote {len(data):,} bytes to {binary_path}")
print("\nRe-signing binary...")
if not resign_binary(binary_path):
_restore_backup("Codesign failed")
sys.exit(1)
print("\nVerifying patched binary...")
if not verify_binary(binary_path):
_restore_backup("Verification failed")
resign_binary(binary_path)
sys.exit(1)
# Final summary
print(f"\n{'='*60}")
print("PATCH APPLIED SUCCESSFULLY")
print(f"{'='*60}")
print(f" Binary: {binary_path}")
print(f" Backup: {bak_path}")
print(f" Applied: {len(all_applied)} patches")
if all_skipped:
print(f" Skipped: {len(all_skipped)} (already applied)")
print()
print("What changed:")
print(f" + 1M context window: ENABLED ({names['statsig_gate']}() patch — Statsig decoupled from analytics)")
print(" + /remote-control: ENABLED")
print(" + Feature gates: ENABLED")
print(f" - 1M race condition: FIXED ({names['extra_usage_check']}() patch — undefined → optimistic allow)")
print()
tg = names.get("telemetry_gate", "telemetry_gate")
ag = names["analytics_gate"]
sg = names["statsig_gate"]
print("Data collection flag status (all env-controlled — flags are fully respected):")
print(" DISABLE_TELEMETRY=1 / CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1:")
print(f" → analytics events: OFF ({ag}→!{tg}()→false, env-controlled)")
print(f" → Segment SDK: BLOCKED (BKK()→!{tg}()→false, env-controlled)")
print(f" → feature gates: ENABLED ({sg}() patch decouples from analytics)")
print(" Without those flags set:")
print(f" → analytics events: ON ({tg}()=false → {ag}()=true)")
print(f" → Segment SDK: WORKS (BKK()=true → Segment proceeds)")
print(f" → feature gates: ENABLED (unchanged)")
print(" DISABLE_ERROR_REPORTING, DISABLE_FEEDBACK_COMMAND, DISABLE_AUTOUPDATER,")
print(" CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY, DISABLE_COST_WARNINGS: fully respected")
print()
print("NOTE: Auto-updates replace the binary and clear patches.")
print(" Re-run this script after each 'claude update' or background auto-update.")
print()
print("To verify, start a new Claude Code session and run:")
print(" /context → should show 1000k max")
print(" /model opus[1m] → should succeed")
print(" /model sonnet[1m] → should succeed (Max plan; requires extra usage on Pro)")
print(" /help → should list /remote-control")
print()
print("To rollback:")
print(f" python3 {__file__} --rollback --real-run")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment