38 KiB
source_url, ingested, sha256, discovered_from
| source_url | ingested | sha256 | discovered_from | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| https://gist.github.com/AdnaneKhan/7a2040bcdebdc923ef73a19f8831132d | 2026-07-01 | 18c3e9f1df9f7496951e816227eaf08155f98c00ae39744b586eff7f314a1026 |
|
Claude Code 2.1.196 — Telemetry / Analytics / Error Reporting Audit
Source: /tmp/claude-2.1.196.bundle.js (18,057,941 bytes, 33,502 lines, minified CJS). Build: VERSION=2.1.196, BUILD_TIME=2026-06-29T00:53:27Z, GIT_SHA=a4ca500badcac68511fb5f04303e32e4360f3dfb.
TL;DR
- There is no Statsig SDK and no Sentry SDK in this bundle. The "Statsig" hit at line 3541 and the "Sentry" hit at line 2846 are both documentation/prose strings (a permission-policy doc and an MCP upsell tip). Anthropic's public docs say "Statsig metrics + Sentry errors"; the 2.1.196 implementation has moved on. Feature flags are served by an internal service called ATIS (cached as
cachedGrowthBookFeatures), and error reporting is Datadog RUM/error-tracking, not Sentry. - Telemetry splits into four outbound pipelines (one opt-in), all rooted in a single in-process sink (
attachAnalyticsSink):- 1P OTLP log events →
https://api.anthropic.com/api/event_logging/v2/batch(the primary "Statsig-equivalent" metrics stream). 2. Datadog logs (events) →https://http-intake.logs.us5.datadoghq.com/api/v2/logs(hard-coded public DD keypubea5604404508cdd34afb69e6f42a05bc). 3. Datadog error tracking (RUM-style) →https://browser-intake-us5-datadoghq.com/api/v2/logs(same key, form-encoded, includes stack frames). 4. 3P OTLP (bring-your-own OTEL backend) — opt-in only, fires only if the user setsOTEL_EXPORTER_OTLP_*/BETA_TRACING_ENDPOINT.
- 1P OTLP log events →
- 1,479 distinct
tengu_*event names are instrumented (full product analytics: tool calls, modes, auto-mode decisions, advisor, adopt, chrome-bridge, api errors, etc.). - Opt-out matrix has one real hole.
DISABLE_TELEMETRY=1cleanly kills pipelines (1) and (3) but does not guard pipeline (2) (Datadog events) — that path is gated only by "is this a firstParty customer" + two server-side toggles. If Anthropic has turned on thetengu_log_datadog_eventsgate for an account,DISABLE_TELEMETRYwill not stop it. - No prompt content, no file contents, no command history, and no shell snapshots are transmitted by any telemetry path. Identifiers are a persistent random
user_id/machine_id,sessionId, account/org UUID, and — notably — a 16-char SHA-256 of the git remote URL (rh) attached to every 1P event.
1. The analytics core (sink plumbing)
The whole telemetry system is a small in-process event bus. Minified names below are shown with their de-obfuscated export aliases where available.
// createAnalyticsState / attachAnalyticsSink / logEvent (bundle byte ~65500, line 12)
function lis() { return { eventQueue: [], sink: null }; } // createAnalyticsState
function _Er(e) { // attachAnalyticsSink (one sink only)
let t = san;
if (t.sink !== null) return;
t.sink = e;
if (t.eventQueue.length > 0) {
let n = t.eventQueue; t.eventQueue = [];
queueMicrotask(() => {
for (let r of n)
r.async ? e.logEventAsync(r.eventName, r.metadata)
: e.logEvent(r.eventName, r.metadata);
});
}
}
function G(e, t) { /* logEvent */ let n = san; if (n.sink === null) { n.eventQueue.push({eventName:e, metadata:t, async:false}); return; } n.sink.logEvent(e, t); }
async function f_(e, t) { /* logEventAsync */ ... n.sink.logEventAsync(e, t); }
The concrete sink is attached by _We() (export: initializeAnalyticsSink):
// line 2025, byte ~7040000
function APp(e, t) { // sink.logEvent
if (Lho) { C(\`logEvent reentered ... dropped ${e}\`, {level:"error"}); return; } // reentry guard
Lho = true;
try {
let n = rIn(e); // per-event sample rate (server-configured)
if (n === 0) return;
let r = n !== null ? { ...t, sample_rate:n } : t;
if (Mho()) mmt(e, SQe(r)); // --> Datadog events pipeline (2)
Lit(e, r); // --> 1P OTLP pipeline (1)
} finally { Lho = false; }
}
async function CPp(e, t) { // sink.logEventAsync
let n = rIn(e); if (n === 0) return;
let r = n !== null ? { ...t, sample_rate:n } : t;
let o = [];
if (Mho()) o.push(mmt(e, SQe(r))); // --> Datadog (2)
o.push(BU(e, r)); // --> 1P OTLP (1), async variant
await Promise.all(o);
}
function _We() { _Er({ logEvent: APp, logEventAsync: CPp }); }
_We() is called unconditionally from three sites: the computer-use MCP bootstrap (OPp), the chrome-bridge MCP bootstrap (Zdf), and the global initSinks() (Bjo, which also wires the error sink rjo). There is no DISABLE_TELEMETRY guard at attach time — gating happens inside each pipeline.
SQe (stripProtoFields) strips fields whose names start with _PROTO_ before any sink sees them — an internal "do not emit" marker.
2. Pipeline (1): 1P OTLP event logging (the "Statsig-equivalent")
Endpoint
// Bzr — OTLP log batch exporter, line 466, byte ~3365300
class Bzr {
constructor(e = {}) {
let t = e.baseUrl
|| (process.env.ANTHROPIC_BASE_URL === "https://api-staging.anthropic.com"
? "https://api-staging.anthropic.com" : "https://api.anthropic.com");
this.endpoint = \`${t}${e.path || "/api/event_logging/v2/batch"}\`;
this.timeout = e.timeout || 10000;
this.maxBatchSize = e.maxBatchSize || 200;
this.maxAttempts = e.maxAttempts ?? 8;
this.skipAuth = e.skipAuth ?? false;
this.isKilled = e.isKilled ?? (() => false); // = () => uqe("firstParty")
...
}
getCurrentBatchFilePath() { return join(bBt(), \`${ZBi}.${It()}.${QBi}.json\`); } // <cfg>/telemetry/...
}
function bBt() { return join(Zn(), "telemetry"); } // persistence dir for failed batches
- Endpoint:
https://api.anthropic.com/api/event_logging/v2/batch(or staging). Path/baseUrl are overridable via the ATIS dynamic configtengu_1p_event_logging_config(oUi()reads it; keyspath,baseUrl,skipAuth,maxAttempts,scheduledDelayMillis,maxExportBatchSize,maxQueueSize). - Sent as OTLP-shaped log records via a
LoggerProvider+BatchLogRecordProcessor. Logger name:com.anthropic.claude_code.events. Resource attrs:service.name=claude-code,service.version=<VERSION>, optionalwsl.version. - Retry/persistence: on export failure the batch is appended to
<configDir>/telemetry/<hash>.<sessionId>.<uuid>.jsonon disk and retried (up to 8 attempts with backoff). These files are local artifacts but contain the same fields as the wire payload. - Server-side kill switch:
isKilled = () => uqe("firstParty"), whereuqereads the dynamic configtengu_frond_boric(a category-keyed boolean map:firstParty,datadog, …).
Emit wrapper
// jzr — builds and emits one OTLP log record, line 470
async function jzr(e, t, n = {}) {
try {
let r = await eIn({ model:n.model, betas:n.betas }); // core_metadata (see §6)
let o = { event_name: t,
event_id: $zr.randomUUID(),
core_metadata: r,
user_metadata: eit(true), // user_metadata (see §6)
event_metadata: n };
let s = x6(); if (s) o.user_id = s; // = deviceId
let i = new Date;
e.emit({ timestamp:i, observedTimestamp:i, body:t, attributes:o });
} catch (r) {}
}
function Lit(e, t = {}) { if (!O6()) return; // <--- master gate
if (!kne) { if (ZY !== null && ZY.length < sUi) ZY.push({eventName:e, metadata:t}); return; }
if (uqe("firstParty")) return; // <--- server-side category kill
jzr(kne, e, t); }
async function BU(e, t = {}) { /* same, async */ }
Gates
O6()=is1PEventLoggingEnabled()=!V9().V9()=cUd() || If() !== null || zge().cUd()=if (CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST) return false; return !kc();— i.e. disabled unless firstParty (or host-managed).If()= gateway URL (using a--gateway/ANTHROPIC_GATEWAY_URLsetup) → disables 1P.zge()=UAs() !== "default"(see §5).
uqe("firstParty")— server-side per-category kill fromtengu_frond_boric.
So pipeline (1) is cleanly killed by: DISABLE_TELEMETRY, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, DO_NOT_TRACK=1, using any 3rd-party LLM provider (Bedrock/Vertex/Foundry/Mantle/AnthropicAWS), or going through a gateway. Confirmed: oIn() (the logger-provider initializer) early-returns if (!O6()) { ZY = null; return; } — when disabled, the provider is never even constructed.
Growthbook experiment exposure
A sibling emitter Wzr (logGrowthBookExperimentTo1P) fires an OTLP record with body: "growthbook_experiment" whenever a user is bucketed into an experiment:
attributes = {
event_type: "GrowthbookExperimentEvent",
event_id, experiment_id, variation_id,
device_id: x6(), // deviceId
account_uuid, organization_uuid,
session_id, user_attributes: { appVersion },
experiment_metadata, environment: "production"
}
Same gates as pipeline (1) (O6() + uqe("firstParty")). Disable via DISABLE_GROWTHBOOK env var as well (the rxu flag).
3. Pipeline (2): Datadog logs (feature events) — the gated-by-server-only one
// mmt — line 11004, byte ~14171286
async function mmt(e, t) {
if (_r() !== "firstParty") return; // firstParty-only (no env-var check!)
let n = stn; if (n === null) n = await gVo(); // fetch DD config (endpoint/key/flush)
if (!n || !qdf.has(e)) return; // event must be in the allow-list
try {
let r = await eIn({ model:t.model, betas:t.betas }),
{ envContext:o, head_sha:s, ...i } = r; // NOTE: strips envContext + head_sha
let a = { ...i, ...o, ...t, userBucket: Vdf() };
if (typeof a.toolName === "string" && a.toolName.startsWith("mcp__")) a.toolName = "mcp";
if (typeof a.model === "string") {
if (!a.model.toLowerCase().includes("claude")) return;
let p = io(Ba(a.model)); a.model = p in F9e ? p : "other";
}
if (typeof a.version === "string") a.version = a.version.replace(/^(\d+\.\d+\.\d+-dev\.\d{8})\.t\d+\.sha[a-f0-9]+$/, "$1");
if (a.status !== undefined && a.status !== null) {
let p = String(a.status); a.http_status = p;
let m = p.charAt(0); if (m >= "1" && m <= "5") a.http_status_range = \`${m}xx\`;
delete a.status;
}
let c = a,
d = { ddsource:"nodejs",
ddtags:[\`event:${e}\`, ...jdf.filter(p => c[p] !== undefined && c[p] !== null)
.map(p => \`${Tfc(p)}:${c[p]}\`)].join(","),
message:e, service:"claude-code", hostname:"claude-code", env:"external" };
for (let [p,m] of Object.entries(a)) if (m !== undefined && m !== null) d[Tfc(p)] = m;
if (otn.push(d), otn.length >= Udf) { if (hRe) clearTimeout(hRe); hRe = null; hVo(); } // flush at 100
else Wdf(); // schedule 15s flush
} catch (r) { Ie(r); }
}
var bfc = "https://http-intake.logs.us5.datadoghq.com/api/v2/logs";
var z4n = "pubea5604404508cdd34afb69e6f42a05bc"; // hard-coded DD *public* API key
var Bdf = 15000, Udf = 100, $df = 5000; // flush interval / batch size / ...
Gates (the important part)
_r() === "firstParty"insidemmt.Mho()at the call site inAPp/CPp:var EPp = "tengu_log_datadog_events"; function Mho() { if (uqe("datadog")) return false; try { return it(EPp, false); } catch { return false; } }uqe("datadog")— server-side kill viatengu_frond_boric.datadog.it("tengu_log_datadog_events", false)— GrowthBook gate, default off.
- The allow-list
qdf(~30 events):tengu_feature_ok/bad/sad,tengu_api_error/success/fallback_last_resort,tengu_auto_mode_*,chrome_bridge_*. Only these names go to Datadog; everything else is 1P-only.
There is no check of DISABLE_TELEMETRY, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, DO_NOT_TRACK, or O6() / zge() anywhere on this path. In practice the gate defaults off, so this is dormant unless Anthropic enables tengu_log_datadog_events for an account/cohort. But if they do, DISABLE_TELEMETRY=1 does not stop it — only the server-side kill switch (uqe("datadog")) or not being firstParty will. This is the single most noteworthy opt-out discrepancy in the bundle.
Notable field handling: this pipeline strips envContext and head_sha from core_metadata before sending (unlike pipeline 1), normalizes model names to a small enum (opus-4-8, sonnet-4-6, …, else "other"), collapses any mcp__* tool name to "mcp", and buckets the user via userBucket: Vdf() (a stable hash).
4. Pipeline (3): Datadog error tracking (RUM-style) — the "Sentry replacement"
There is no Sentry SDK in the bundle. Verified absent: @sentry/*, captureException, captureMessage, addBreadcrumb, beforeSend, sentry.io, any DSN URL. The single prose "Sentry" mention (line 2846) is in an MCP-upsell tip ("MCP connects Claude to … Sentry …").
Error capture flows through Ie(err) → Ste.logError(err) (singleton set by qAs):
// Ie — the public reportError, line 139
function Ie(e) {
let t = er(e);
try {
if (ct(process.env.CLAUDE_CODE_USE_BEDROCK) || ct(process.env.CLAUDE_CODE_USE_VERTEX)
|| ct(process.env.CLAUDE_CODE_USE_FOUNDRY) || ct(process.env.CLAUDE_CODE_USE_ANTHROPIC_AWS)
|| ct(process.env.CLAUDE_CODE_USE_MANTLE) || process.env.DISABLE_ERROR_REPORTING || zi())
return;
let r = { error: t.stack || t.message, timestamp: new Date().toISOString() };
if (_Nu(r), Ste === null) { zet.push({type:"error", error:t}); return; }
Ste.logError(t);
} catch {}
}
// Sink wired in initSinks (Bjo -> rjo), line 9053
function AZm(e) { // logError
pXi(e); // -> Jc("internal_error", {error_name, error_code}) [pipeline 4 if configured]
Wjt(e); // -> Datadog error-tracking (this pipeline)
let t = e.stack || e.message, n = "";
if (mo.isAxiosError(e) && e.config?.url) { // *** axios failures include url+status+body ***
let r = [\`url=${e.config.url}\`];
if (e.response?.status !== undefined) r.push(\`status=${e.response.status}\`);
let o = EZm(e.response?.data); if (o) r.push(\`body=${o}\`);
n = \`[${r.join(", ")}] \`;
}
C(\`${e.name}: ${n}${t}\`, {level:"error"});
bZm(tjo(), { error: \`${n}${t}\` }); // bZm is a NO-OP (\`function bZm(e,t){return}\`) in this build
}
Wjt builds a Datadog error-tracking payload and batches it:
// BUa — gate for error tracking, line 2684
function BUa() {
if (process.env.DISABLE_ERROR_REPORTING) return false;
if (zge()) return false; // ANY non-default traffic mode disables this
if (_r() !== "firstParty" || !bu()) return false; // firstParty + real anthropic base URL only
if (!Y4n.gte(VERSION, <min-version>)) return false;
...
return true;
}
function Wjt(e, t = "logError") {
if (!BUa()) return;
try {
let n = er(e);
if (t === "logError" && sBp(n)) return; // noisy-error blocklist
if ((t === "unhandled_rejection" || t === "uncaught_exception") && oBp(n)) return;
if (Eyo()) return; // rate-limit / dedupe
let r = eBp(n, t); // build payload
Ayo(r); // batch -> DD
} catch {}
}
var NUa = "https://browser-intake-us5-datadoghq.com/api/v2/logs";
var wFp = 30000, xFp = 25, bft = 100; // flush 30s / batch 25 / per-process cap 100
// IFp POSTs as URLSearchParams: ddsource=browser, dd-api-key=z4n, dd-evp-origin=browser,
// dd-evp-origin-version=<VERSION>
What an error payload contains (eBp)
{
ddtags: \`service:claude-code-error-tracking,team:claude-code,version:<v>,env:external,
origin:<logError|unhandled_rejection|uncaught_exception>,platform:<wsl|darwin|...>,
os_release:<x.y>,user_bucket:<hash>,entrypoint:<cli|sdk-cli|...>,
node_version:<v>,bun_version:1.4.0,is_native_runtime:<bool>[,model:<m>][,error_code:<c>]
[,session_kind:..][,has_attacher:..][,renderer_mode:..]\`,
service: "claude-code-error-tracking",
hostname: "claude-code",
status: "error",
message: "<ErrorName>: <redacted message>".slice(0, 4000), // *** message is redacted (see below)
timestamp,
error: {
kind: <ErrorName>,
message: <redacted>.slice(0, 4000),
stack: a.formatted.slice(0, 16000), // *** up to 16KB of stack trace
fingerprint: u, // dedupe hash
handling: "handled" | "unhandled"
},
version, sourcemap_group: "darwin", env: "external",
user_bucket, origin, host_platform, host_os_release,
host_name_redacted: Gjt().slice(0, 12), // first 12 hex of machineID
entrypoint, node_version, bun_version,
..., model ...,
error_frames: a.frames.slice(0, 20), // top 20 frames {file, function, ...}
feature_flags: QFp() // current gate/experiment state
}
Redaction applied to messages and stacks
N3(msg)(line 1560, byte ~5421022): truncates to 4000 chars; rewrites://user:pass@→://<userinfo>@; replaces git URLs containing credentials →<url>; then runs a chain of regex scrubbers (ahp,thp,shp,php,Xfp,Yfp,chp,lhp,dhp) that strip emails, IPs (v4/v6), phone numbers, and similar PII patterns.fma(err, msg)(line 1560): if the error object has.path/.deststrings (FS tool errors), those literal paths are replaced with the token<path>in the message before redaction.- A quirky marker: error-name normalization strips a literal suffix
_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(t2e(n.replace(/_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS$/, ""))) — an internal convention for dev-asserted "clean" error names. host_name_redactedis the first 12 hex chars of the machineID — not the hostname.
Stack frames are sent as-is (top 20, capped at 16KB total). File paths in frames are not globally redacted (only err.path / err.dest -style values fed through fma). So a stack frame like at foo (/Users/<you>/secret-repo/file.js:12:3) will reach Datadog if it appears in the stack string. This is the main residual content-leak risk in the error path.
Gates (clean, unlike pipeline 2)
BUa() returns false if any of: DISABLE_ERROR_REPORTING, zge() (i.e. DISABLE_TELEMETRY / CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC / DO_NOT_TRACK), non-firstParty provider, non-anthropic base URL, or version below the floor. So error tracking is properly killed by both DISABLE_ERROR_REPORTING and the telemetry/non-essential env vars.
5. Pipeline (4): 3P OTLP (opt-in, user-configured)
Jc(eventName, attrs) is the 3P event logger. It emits OTLP-shaped records with body claude_code.<eventName> to whatever OTLP LoggerProvider the user configured via standard OTEL_EXPORTER_OTLP_* environment. If no 3P exporter is wired, events are dropped with a warn-level log:
async function Jc(e, t = {}) {
let n = { ...j6e(), "event.name": e, "event.timestamp": new Date().toISOString(),
"event.sequence": ZQd++ };
let r = $Ue(); if (r) n["prompt.id"] = r; // *** prompt correlation id ***
if (process.env.CLAUDE_CODE_WORKSPACE_HOST_PATHS) n["workspace.host_paths"] = ...;
for (let [l,c] of Object.entries(t)) if (c !== undefined) n[l] = c;
let i = { timestamp:s, observedTimestamp:s, body:\`claude_code.${e}\`, attributes:n };
let a = XSr(); // = Bt.eventLogger (set by Oin())
if (a) { a.emit(i); return; }
if (!QSr(i) && !uXi) uXi = true, C(\`[3P telemetry] Event dropped (no event logger initialized): ${e}\`, {level:"warn"});
}
function j6e() { // common 3P attributes
let e = x6(), t = It(), n = IMn(), r = Object.keys(n).length > 0, o = {};
if (C$t("OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES"))
for (let [i,a] of Object.entries(XQd(process.env.OTEL_RESOURCE_ATTRIBUTES))) {
if (r && (i.startsWith("user.") || i.startsWith("identity."))) continue; // respect identity attrs
o[i] = a;
}
if (o["user.id"] = e, C$t("OTEL_METRICS_INCLUDE_SESSION_ID")) {
if (o["session.id"] = t, process.env.CLAUDE_CODE_REMOTE_SESSION_ID) o["ccr.session.id"] = ...;
}
if (C$t("OTEL_METRICS_INCLUDE_VERSION")) o["app.version"] = VERSION;
...
}
Activation is explicit: the exporter is only constructed when the user sets OTEL_EXPORTER_OTLP_LOGS_PROTOCOL / OTEL_EXPORTER_OTLP_ENDPOINT (or BETA_TRACING_ENDPOINT for traces). Without those env vars, XSr() stays null and Jc drops events. pXi(err) (invoked from the error sink AZm) calls Jc("internal_error", {error_name, error_code}) — so internal-error summaries also flow here when configured.
6. Data fields sent (per pipeline)
core_metadata (eIn, line 466, attached to every 1P and Datadog event)
model, sessionId, userType:"external",
betas (comma-joined),
envContext: { // = a2d() → flattened by XBi:
platform, platform_raw, arch, node_version, terminal, shell,
package_managers, runtimes, is_running_with_bun, is_ci, is_claubbit,
is_claude_code_remote, is_local_agent_mode, is_conductor, is_github_action,
is_claude_code_action, is_claude_ai_auth, version, build_time,
deployment_environment, remote_environment_type, claude_code_container_id,
claude_code_remote_session_id
},
entrypoint (CLAUDE_CODE_ENTRYPOINT),
sessionKind, hasAttacher,
agentSdkVersion (CLAUDE_AGENT_SDK_VERSION),
isInteractive, clientType,
processMetrics (cpu/memory),
sweBenchRunId/sweBenchInstanceId/sweBenchTaskId (env vars, usually empty),
subscriptionType, rateLimitTier,
rh, // *** 16-char SHA-256 of normalized git remote URL (qfn) ***
head_sha (ONLY if CLAUDE_CODE_ENABLE_FEEDBACK_SURVEY_FOR_OTEL is set),
rendererMode
rh derivation:
function qfn() { let e = await Vz(); if (!e) return null; // Vz() = git remote.origin.url
let t = n_e(e); if (!t) return null; // n_e: strip git@/https://user@, .git, lowercase
return createHash("sha256").update(t).digest("hex").substring(0, 16); }
i.e. rh = sha256("github.com/owner/repo").slice(0,16). A stable, per-repo identifier attached to every event. Not the literal URL, but correlatable across sessions and accounts.
user_metadata (eit, line 444)
deviceId (x6 — 32-byte hex, persisted across runs in the config file),
sessionId,
email: JLd() === undefined always, // email collection is stubbed out in this build
appVersion, platform,
organizationUuid, accountUuid (from Nc() — claude.ai account),
userType:"external",
subscriptionType, rateLimitTier, firstTokenTime,
githubActionsMetadata { actor, actorId, repository, repositoryId,
repositoryOwner, repositoryOwnerId } // *** only when GITHUB_ACTIONS=true ***
The GitHub-Actions block is worth calling out: when CC runs inside GitHub Actions, every event carries the actor handle, actor ID, full owner/repo string, repo numeric ID, and owner numeric ID. This is far more identifying than the other fields and is gated only by the same O6() master switch (so DISABLE_TELEMETRY kills it; nothing short of that does).
Identifiers in brief
user_id/deviceId=x6()= 32 random bytes hex, persisted in the local settings file (Ot().userID); lazily generated on first event.machineID=Gjt()= 32 random bytes hex, persisted.sessionId=It().accountUuid/organizationUuidfrom the claude.ai account session (only when authenticated against api.anthropic.com).- No email is collected (
JLdreturns undefined). - No prompt content, file content, command history, or cwd path is sent by any pipeline. (
cwdappears only in the local MCP-error/mcp-debug JSONL writersCZm/RZm, which write to disk undermcp-logs-<server>/, not over the network.)
7. Opt-out matrix (the deliverable)
Pipelines:
- 1P = OTLP events to
api.anthropic.com/api/event_logging/v2/batch(incl. Growthbook experiment exposures) - DD-EVT = Datadog logs to
http-intake.logs.us5.datadoghq.com(events) - DD-ERR = Datadog error tracking to
browser-intake-us5-datadoghq.com(errors + stacks) - 3P = user-configured OTLP backend (opt-in)
| Env var / condition | 1P (Lit/BU) | DD-EVT (mmt) | DD-ERR (Wjt) | 3P (Jc) | Notes |
|---|---|---|---|---|---|
| no env vars set (default firstParty) | ON | gated by tengu_log_datadog_events (default OFF) |
ON | OFF (no exporter) | normal operation |
DISABLE_TELEMETRY=1 |
OFF | still ON if DD-EVT gate is server-on | OFF (via zge) |
unaffected | hole |
DISABLE_ERROR_REPORTING=1 |
unaffected | unaffected | OFF | unaffected | clean |
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 |
OFF | still ON if DD-EVT gate is server-on | OFF | unaffected | hole (same as above) |
DO_NOT_TRACK=1 |
OFF | still ON if DD-EVT gate is server-on | OFF | unaffected | hole |
DISABLE_GROWTHBOOK=1 |
OFF (experiments only) | unaffected | unaffected | unaffected | |
CLAUDE_CODE_USE_BEDROCK/VERTEX/FOUNDRY/MANTLE/ANTHROPIC_AWS=1 |
OFF (_r()!=firstParty) |
OFF (_r()!=firstParty) |
OFF | unaffected | all 1P/DD telemetry dies for 3rd-party LLM users |
ANTHROPIC_BASE_URL to non-api.anthropic.com host |
OFF (!bu()) |
unaffected (still firstParty by _r) |
OFF (!bu()) |
unaffected | |
Going through --gateway (If()) |
OFF | unaffected | unaffected | unaffected | |
Server-side tengu_frond_boric.firstParty=true |
OFF (extra kill) | unaffected | unaffected | unaffected | Anthropic-side |
Server-side tengu_frond_boric.datadog=true |
unaffected | OFF | unaffected | unaffected | Anthropic-side DD-EVT kill |
OTEL_EXPORTER_OTLP_ENDPOINT unset |
— | — | — | OFF (opt-in) | 3P stays dormant |
The hole, precisely: DISABLE_TELEMETRY, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, and DO_NOT_TRACK all route through UAs() / zge(), which guards pipelines 1P and DD-ERR but not DD-EVT. DD-EVT is gated only by _r()==="firstParty" + Mho() (server toggle). So a firstParty user for whom Anthropic has enabled tengu_log_datadog_events will still emit Datadog feature events even with all three user-facing opt-out env vars set.
Traffic that is never gated by these env vars (by design — "essential")
POST https://api.anthropic.com/v1/messages(and/v1/messages?beta=...) — the LLM API itself.https://api.anthropic.com/v1/sessions*,/v1/agents*,/v1/files*,/v1/environments*,/v1/deployments*,/v1/design/mcp— Claude platform API (sessions, agents, files, environments).https://api.anthropic.com/api/oauth/claude_cli/*— CLI auth/OAuth (create_api_key, roles).https://api.anthropic.com/api/web/domain_info— domain info lookup.https://api.anthropic.com/api/claude_code/discovery/team_usage— team skills/MCP discovery (additionally gated byallow_team_discoverypermission +tengu_team_discoverygate +Eo()logged-in).https://claude.ai/oauth/claude-code-client-metadata,https://claude.ai/install.sh,https://downloads.claude.ai/claude-code-releases*— installer / OAuth metadata.https://storage.googleapis.com/claude-code-dist-.../plugin-stats/plugin-details.json— plugin marketplace details.- Auto-updater hits to
downloads.claude.ai/claude-code-releases(unlessDISABLE_UPDATES/DISABLE_AUTOUPDATER). - Cloud-provider OAuth (
oauth2.googleapis.com/token/tokeninfo/revoke,cloudresourcemanager.googleapis.com,aiplatform*.googleapis.com) — only when using Vertex. - No prompt-embedded steganography or watermarking was found.
tengu_canarysounds suspicious but is just the native-installer update channel (a version string served from a GrowthBook config).watermarkoccurrences are all UI/screen-recording overlays. There is no code that injects tracking tokens into prompts or API request bodies.
8. Outbound network destinations referenced in the bundle (filtered to telemetry/analytics/auth/host infra)
| Host / URL | Purpose | Gated by opt-out? |
|---|---|---|
https://api.anthropic.com/api/event_logging/v2/batch |
1P OTLP telemetry (pipeline 1) | yes (DISABLE_TELEMETRY etc.) |
https://http-intake.logs.us5.datadoghq.com/api/v2/logs |
Datadog feature events (pipeline 2) | partial — server gate only, not user env vars |
https://browser-intake-us5-datadoghq.com/api/v2/logs |
Datadog error tracking (pipeline 3) | yes (DISABLE_ERROR_REPORTING and telemetry env vars) |
user-configured OTLP endpoint (OTEL_EXPORTER_OTLP_ENDPOINT, BETA_TRACING_ENDPOINT) |
3P telemetry (pipeline 4) | n/a (opt-in) |
https://api.anthropic.com/v1/messages, /v1/sessions, /v1/agents, /v1/files, /v1/environments, /v1/deployments, /v1/design/mcp, /api/web/domain_info, /api/oauth/claude_cli/*, /api/claude_code/discovery/team_usage |
Claude platform API / auth | no (essential) |
https://api-staging.anthropic.com |
staging variant of all the above | no (essential when BASE_URL=staging) |
https://mcp-proxy.anthropic.com |
MCP proxy | no (essential when configured) |
https://claude.ai, https://claude.ai/oauth/claude-code-client-metadata, https://downloads.claude.ai/claude-code-releases*, https://claude.ai/install.sh |
install / OAuth / onboarding | no (essential) |
https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/plugin-stats/plugin-details.json |
plugin marketplace metadata fetch | no |
https://api.datadoghq.com/mcp, https://api.githubcopilot.com/mcp, https://mcp.sentry.dev/mcp, https://api.notion.com/v1/oauth/token, https://slack.com/api/oauth.v2.access, https://api.github.com, https://api.github.com/graphql, https://api.example.com/mcp, https://app.corridor.dev/api/mcp |
MCP server URLs — only hit if the user configures them as MCP servers; not automatic | n/a |
https://aiplatform.googleapis.com, https://oauth2.googleapis.com/*, https://cloudresourcemanager.googleapis.com/*, https://www.googleapis.com/oauth2/*, https://admin.googleapis.com/admin/directory/v1/groups |
Vertex AI / GCP OAuth | only when CLAUDE_CODE_USE_VERTEX |
https://status.anthropic.com, https://support.anthropic.com, https://www.anthropic.com/legal/*, https://docs.anthropic.com/..., https://platform.claude.com/docs/..., https://code.claude.com/docs/..., https://github.com/anthropics/* |
doc / status / legal links — opened in browser on demand, not hit by the runtime | n/a |
No hits for: statsigapi.net, featuregates.org, api.statsig.com, ingest.sentry.io, o*.ingest.sentry.io, amplitude.com, api.segment.io, api.mixpanel.com, track.posthog.com, api.rudderstack.com, insights.collector.newrelic.com. The only third-party analytics hosts are the two Datadog ones above.
9. "Undisclosed collection" assessment (vs Anthropic's public data-usage docs)
Anthropic's published docs say telemetry consists of "Statsig metrics" and "Sentry errors", explicitly excluding code contents and file paths. Compared to that, the 2.1.196 bundle shows:
- No Statsig and no Sentry are bundled. The actual implementation is (a) a 1P OTLP log stream to
api.anthropic.com, (b) Datadog for both feature events and error tracking, and (c) optionally Growthbook for experiment exposures. The spirit of the docs (metrics + errors) is preserved, but the named vendors are wrong/incomplete. Medium disclosure gap. - Hard-coded Datadog public key
pubea5604404508cdd34afb69e6f42a05bcships in the bundle, sending tous5.datadoghq.com. Datadog as a recipient of Claude Code usage data is not prominently disclosed. Medium gap. rh— a 16-char SHA-256 of the git remote URL is attached to every 1P event. This is a stable repo identifier. It is not "code or paths" in the literal sense (no filename, no content), but it does let Anthropic see, per event, which repository the user is working in. Close to the line of what the docs say is excluded; worth disclosure. Low-medium gap.- GitHub-Actions context block (
actor,actorId,repository,repositoryId,repositoryOwner,repositoryOwnerId) attached to every event whenGITHUB_ACTIONS=true. The literalowner/repostring is sent here (not hashed, unlikerh). Same caveat as (3); more identifying. Low-medium gap. - Stack traces (up to 16KB, top 20 frames) are sent to Datadog on errors.
N3/fmaredacts credentials, emails, IPs, anderr.path/err.destvalues, but file paths that appear inside stack frames as(/path/to/file.js:line:col)are not scrubbed. The docs say no file paths are sent; stack frames can carry them. This is the most concrete content-leak risk in the whole telemetry surface. Medium gap. prompt.idis attached to 3P telemetry events (Jc), correlating metrics to specific prompts. Prompt content is not sent. Borderline; arguably fine.DISABLE_TELEMETRYdoes not cover the DD-EVT pipeline (§7 hole). A user who setsDISABLE_TELEMETRY=1reasonably believes all usage metrics stop. For the (currently dormant, gate-default-off) Datadog feature-events pipeline, they don't. High disclosure gap if Anthropic ever turnstengu_log_datadog_eventson broadly; today it is inert.- Server-side dynamic config can re-target telemetry at runtime:
tengu_frond_boric(category kill switches),tengu_1p_event_logging_config(can override endpoint,skipAuth, batch params, retry count),tengu_event_*_sampling(per-event sample rates),tengu_log_datadog_events(DD-EVT on/off). The endpoint-overridability of the 1P exporter means Anthropic could in principle repoint event collection without a client update. Operational, not necessarily a disclosure issue, but worth noting for threat modeling.
Confirmed not collected / not sent
- No prompt or completion text.
- No file contents, no diffs, no command history, no shell snapshots.
- No
cwdpath over the network (it appears only in local on-disk MCP debug logs). - No email (
JLdis a no-op returning undefined). - No prompt-embedded tracking tokens / steganography / canary watermarks.
10. Code excerpts (verbatim, de-obfuscated where aliases are known)
Analytics sink attach (unconditional)
// Bjo — initSinks, line 9104
function Bjo() { rjo(); _We(); } // rjo = error sink, _We = analytics sink; NO traffic-mode check
Master gates
function UAs() { // traffic mode resolver, line 139
if (process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC) return "essential-traffic";
if (process.env.DISABLE_TELEMETRY) return "no-telemetry";
if (ct(process.env.DO_NOT_TRACK)) return "no-telemetry";
return "default";
}
function zi() { return UAs() === "essential-traffic"; }
function zge() { return UAs() !== "default"; } // disables 1P + DD-ERR
function cUd() { // firstParty check
if (ct(process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST)) return false;
return !kc(); // kc = (_r()==="firstParty")
}
function V9() { return cUd() || If() !== null || zge(); } // 1P disabled if true
function O6() { return !V9(); } // is1PEventLoggingEnabled
function _r() { // provider tier, line 260
if (If()) return "gateway";
if (ct(process.env.CLAUDE_CODE_USE_BEDROCK)) return "bedrock";
if (ct(process.env.CLAUDE_CODE_USE_FOUNDRY)) return "foundry";
if (ct(process.env.CLAUDE_CODE_USE_ANTHROPIC_AWS)) return "anthropicAws";
if (ct(process.env.CLAUDE_CODE_USE_MANTLE)) return "mantle";
if (ct(process.env.CLAUDE_CODE_USE_VERTEX)) return "vertex";
return "firstParty";
}
function uqe(e) { return i0("tengu_frond_boric", {})?.[e] === true; } // server-side category kill
function Mho() { // shouldTrackDatadog
if (uqe("datadog")) return false;
try { return it("tengu_log_datadog_events", false); } catch { return false; }
}
function BUa() { // DD-ERR gate, line 2684
if (process.env.DISABLE_ERROR_REPORTING) return false;
if (zge()) return false;
if (_r() !== "firstParty" || !bu()) return false;
if (!Y4n.gte(VERSION, <min>)) return false;
/* ... */ return true;
}
Identifiers
function x6() { // deviceId, line 11004
let e = Ot(); if (e.userID) return e.userID;
if (nVo) return nVo;
let t = randomBytes(32).toString("hex"); nVo = t;
try { _n(n => ({ ...n, userID: t })); } catch { /* ... */ }
return t;
}
// Gjt() is identical for machineID.
Datadog key + endpoints
var bfc = "https://http-intake.logs.us5.datadoghq.com/api/v2/logs"; // pipeline 2 (events)
var NUa = "https://browser-intake-us5-datadoghq.com/api/v2/logs"; // pipeline 3 (errors)
var z4n = "pubea5604404508cdd34afb69e6f42a05bc"; // hard-coded public DD key
1P OTLP endpoint
// Bzr constructor
this.endpoint = \`${e.baseUrl || "https://api.anthropic.com"}${e.path || "/api/event_logging/v2/batch"}\`;