security: kill GrowthBook phone-home and auto-updater at build time

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.
This commit is contained in:
Mikey
2026-04-01 21:29:12 -07:00
parent 1a60509fdc
commit 0746802b6a
2 changed files with 223 additions and 0 deletions

View File

@@ -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) {

View File

@@ -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<string, string> = {
// ─── 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`)
},
}