From 0746802b6a08c00053a28b3519f7946995b239ad Mon Sep 17 00:00:00 2001 From: Mikey <5217366+BrainSlugs83@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:29:12 -0700 Subject: [PATCH] security: kill GrowthBook phone-home and auto-updater at build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Bun build plugin that replaces analytics/telemetry modules with no-op stubs at compile time. Primary targets (NOT killed by PR #94 or the feature() shim): - GrowthBook: phones home to api.anthropic.com on every launch, sending account UUID, org UUID, email, device ID, subscription type. Refreshes every 6 hours. Now returns defaults without making any network call. - Auto-updater: contacts storage.googleapis.com and npm registry on launch to check for new versions. Now returns null/no-op. Defense-in-depth (already gated by PR #94 or feature flags, but now the code itself is replaced with empty functions): - Datadog, 1P event logging, BigQuery metrics, Perfetto tracing, session tracing, plugin fetch telemetry, transcript sharing. Deliberately NOT stubbed: - Plugin marketplace (downloads.claude.ai) — needed for /plugin - User-configurable OTel (CLAUDE_CODE_ENABLE_TELEMETRY) — opt-in Implementation: separate plugin file (scripts/no-telemetry-plugin.ts) with a 2-line hook in build.ts. The plugin file does not exist upstream so it cannot cause merge conflicts. --- scripts/build.ts | 2 + scripts/no-telemetry-plugin.ts | 221 +++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 scripts/no-telemetry-plugin.ts diff --git a/scripts/build.ts b/scripts/build.ts index 0a00f2c9..137fadb6 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -9,6 +9,7 @@ */ import { readFileSync } from 'fs' +import { noTelemetryPlugin } from './no-telemetry-plugin' const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) const version = pkg.version @@ -64,6 +65,7 @@ const result = await Bun.build({ 'MACRO.NATIVE_PACKAGE_URL': 'undefined', }, plugins: [ + noTelemetryPlugin, { name: 'bun-bundle-shim', setup(build) { diff --git a/scripts/no-telemetry-plugin.ts b/scripts/no-telemetry-plugin.ts new file mode 100644 index 00000000..dc83b50f --- /dev/null +++ b/scripts/no-telemetry-plugin.ts @@ -0,0 +1,221 @@ +/** + * No-Telemetry Build Plugin for OpenClaude + * + * Replaces all analytics, telemetry, and phone-home modules with no-op stubs + * at compile time. Zero runtime cost, zero network calls to Anthropic. + * + * This file is NOT tracked upstream — merge conflicts are impossible. + * Only build.ts needs a one-line import + one-line array entry. + * + * Kills: + * - GrowthBook remote feature flags (api.anthropic.com) + * - Datadog event intake + * - 1P event logging (api.anthropic.com/api/event_logging/batch) + * - BigQuery metrics exporter (api.anthropic.com/api/claude_code/metrics) + * - Perfetto / OpenTelemetry session tracing + * - Auto-updater (storage.googleapis.com, npm registry) + * - Plugin fetch telemetry + * - Transcript / feedback sharing + */ + +import type { BunPlugin } from 'bun' + +// Module path (relative to src/, without extension) → stub source +const stubs: Record = { + + // ─── Analytics core ───────────────────────────────────────────── + + 'services/analytics/index': ` +export function stripProtoFields(metadata) { return metadata; } +export function attachAnalyticsSink() {} +export function logEvent() {} +export async function logEventAsync() {} +export function _resetForTesting() {} +`, + + 'services/analytics/growthbook': ` +const noop = () => {}; +export function onGrowthBookRefresh() { return noop; } +export function hasGrowthBookEnvOverride() { return false; } +export function getAllGrowthBookFeatures() { return {}; } +export function getGrowthBookConfigOverrides() { return {}; } +export function setGrowthBookConfigOverride() {} +export function clearGrowthBookConfigOverrides() {} +export function getApiBaseUrlHost() { return undefined; } +export const initializeGrowthBook = async () => null; +export async function getFeatureValue_DEPRECATED(feature, defaultValue) { return defaultValue; } +export function getFeatureValue_CACHED_MAY_BE_STALE(feature, defaultValue) { return defaultValue; } +export function getFeatureValue_CACHED_WITH_REFRESH(feature, defaultValue) { return defaultValue; } +export function checkStatsigFeatureGate_CACHED_MAY_BE_STALE() { return false; } +export async function checkSecurityRestrictionGate() { return false; } +export async function checkGate_CACHED_OR_BLOCKING() { return false; } +export function refreshGrowthBookAfterAuthChange() {} +export function resetGrowthBook() {} +export async function refreshGrowthBookFeatures() {} +export function setupPeriodicGrowthBookRefresh() {} +export function stopPeriodicGrowthBookRefresh() {} +export async function getDynamicConfig_BLOCKS_ON_INIT(configName, defaultValue) { return defaultValue; } +export function getDynamicConfig_CACHED_MAY_BE_STALE(configName, defaultValue) { return defaultValue; } +`, + + 'services/analytics/sink': ` +export function initializeAnalyticsGates() {} +export function initializeAnalyticsSink() {} +`, + + 'services/analytics/config': ` +export function isAnalyticsDisabled() { return true; } +export function isFeedbackSurveyDisabled() { return true; } +`, + + 'services/analytics/datadog': ` +export const initializeDatadog = async () => false; +export async function shutdownDatadog() {} +export async function trackDatadogEvent() {} +`, + + 'services/analytics/firstPartyEventLogger': ` +export function getEventSamplingConfig() { return {}; } +export function shouldSampleEvent() { return null; } +export async function shutdown1PEventLogging() {} +export function is1PEventLoggingEnabled() { return false; } +export function logEventTo1P() {} +export function logGrowthBookExperimentTo1P() {} +export function initialize1PEventLogging() {} +export async function reinitialize1PEventLoggingIfConfigChanged() {} +`, + + 'services/analytics/firstPartyEventLoggingExporter': ` +export class FirstPartyEventLoggingExporter { + constructor() {} + async export(logs, resultCallback) { resultCallback({ code: 0 }); } + async getQueuedEventCount() { return 0; } + async shutdown() {} + async forceFlush() {} +} +`, + + 'services/analytics/metadata': ` +export function sanitizeToolNameForAnalytics(toolName) { return toolName; } +export function isToolDetailsLoggingEnabled() { return false; } +export function isAnalyticsToolDetailsLoggingEnabled() { return false; } +export function mcpToolDetailsForAnalytics() { return {}; } +export function extractMcpToolDetails() { return undefined; } +export function extractSkillName() { return undefined; } +export function extractToolInputForTelemetry() { return undefined; } +export function getFileExtensionForAnalytics() { return undefined; } +export function getFileExtensionsFromBashCommand() { return undefined; } +export async function getEventMetadata() { return {}; } +export function to1PEventFormat() { return {}; } +`, + + // ─── Telemetry subsystems ─────────────────────────────────────── + + 'utils/telemetry/bigqueryExporter': ` +export class BigQueryMetricsExporter { + constructor() {} + async export(metrics, resultCallback) { resultCallback({ code: 0 }); } + async shutdown() {} + async forceFlush() {} + selectAggregationTemporality() { return 0; } +} +`, + + 'utils/telemetry/perfettoTracing': ` +export function initializePerfettoTracing() {} +export function isPerfettoTracingEnabled() { return false; } +export function registerAgent() {} +export function unregisterAgent() {} +export function startLLMRequestPerfettoSpan() { return ''; } +export function endLLMRequestPerfettoSpan() {} +export function startToolPerfettoSpan() { return ''; } +export function endToolPerfettoSpan() {} +export function startUserInputPerfettoSpan() { return ''; } +export function endUserInputPerfettoSpan() {} +export function emitPerfettoInstant() {} +export function emitPerfettoCounter() {} +export function startInteractionPerfettoSpan() { return ''; } +export function endInteractionPerfettoSpan() {} +export function getPerfettoEvents() { return []; } +export function resetPerfettoTracer() {} +export async function triggerPeriodicWriteForTesting() {} +export function evictStaleSpansForTesting() {} +export const MAX_EVENTS_FOR_TESTING = 0; +export function evictOldestEventsForTesting() {} +`, + + 'utils/telemetry/sessionTracing': ` +const noopSpan = { + end() {}, setAttribute() {}, setStatus() {}, + recordException() {}, addEvent() {}, isRecording() { return false; }, +}; +export function isBetaTracingEnabled() { return false; } +export function isEnhancedTelemetryEnabled() { return false; } +export function startInteractionSpan() { return noopSpan; } +export function endInteractionSpan() {} +export function startLLMRequestSpan() { return noopSpan; } +export function endLLMRequestSpan() {} +export function startToolSpan() { return noopSpan; } +export function startToolBlockedOnUserSpan() { return noopSpan; } +export function endToolBlockedOnUserSpan() {} +export function startToolExecutionSpan() { return noopSpan; } +export function endToolExecutionSpan() {} +export function endToolSpan() {} +export function addToolContentEvent() {} +export function getCurrentSpan() { return null; } +export async function executeInSpan(spanName, fn) { return fn(noopSpan); } +export function startHookSpan() { return noopSpan; } +export function endHookSpan() {} +`, + + // ─── Auto-updater (phones home to GCS + npm) ────────────────── + + 'utils/autoUpdater': ` +export async function assertMinVersion() {} +export async function getMaxVersion() { return undefined; } +export async function getMaxVersionMessage() { return undefined; } +export function shouldSkipVersion() { return true; } +export function getLockFilePath() { return '/tmp/openclaude-update.lock'; } +export async function checkGlobalInstallPermissions() { return { hasPermissions: false, npmPrefix: null }; } +export async function getLatestVersion() { return null; } +export async function getNpmDistTags() { return { latest: null, stable: null }; } +export async function getLatestVersionFromGcs() { return null; } +export async function getGcsDistTags() { return { latest: null, stable: null }; } +export async function getVersionHistory() { return []; } +export async function installGlobalPackage() { return 'success'; } +`, + + // ─── Plugin fetch telemetry (not the marketplace itself) ─────── + + 'utils/plugins/fetchTelemetry': ` +export function logPluginFetch() {} +export function classifyFetchError() { return 'disabled'; } +`, + + // ─── Transcript / feedback sharing ───────────────────────────── + + 'components/FeedbackSurvey/submitTranscriptShare': ` +export async function submitTranscriptShare() { return { success: false }; } +`, +} + +export const noTelemetryPlugin: BunPlugin = { + name: 'no-telemetry', + setup(build) { + for (const [modulePath, contents] of Object.entries(stubs)) { + // Build regex that matches the resolved file path on any OS + // e.g. "services/analytics/growthbook" → /services[/\\]analytics[/\\]growthbook\.(ts|js)$/ + const escaped = modulePath + .replace(/\//g, '[/\\\\]') + .replace(/\./g, '\\.') + const filter = new RegExp(`${escaped}\\.(ts|js)$`) + + build.onLoad({ filter }, () => ({ + contents, + loader: 'js', + })) + } + + console.log(` 🔇 no-telemetry: stubbed ${Object.keys(stubs).length} modules`) + }, +}