DISABLE_TELEMETRY=0, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=0, DISABLE_ERROR_REPORTING=0 silently opt you OUT — "0" is read as truthy
Summary
These three variables are tested with raw JS string truthiness instead of Claude Code's own boolean parser. "0" is a non-empty string, so it takes the same branch as "1": setting them to "0" — the documented off value — opts the user out.
The published schema (json.schemastore.org/claude-code-settings.json) declares all three as "enum": ["0","1"], so "0" is advertised as valid and off. Authoring managed-settings.json against the schema walks straight into this.
Version: 2.1.223, reproduced in node:22-slim with no managed-settings file present.
Reproduction
docker run --rm -i node:22-slim bash -s <<'EOF'
npm i -g @anthropic-ai/claude-code@2.1.223 >/dev/null 2>&1
probe() {
echo "--- $1 ---"; shift
env -u DISABLE_TELEMETRY -u CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC -u DO_NOT_TRACK \
"$@" claude doctor 2>&1 | grep -i "Feature-flag evaluation disabled" || true
}
probe "ALL UNSET (control)"
probe "DISABLE_TELEMETRY=0" DISABLE_TELEMETRY=0
probe "DISABLE_TELEMETRY=1" DISABLE_TELEMETRY=1
probe "NONESSENTIAL=0" CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=0
probe "NONESSENTIAL=1" CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
probe "DO_NOT_TRACK=0" DO_NOT_TRACK=0
probe "DO_NOT_TRACK=1" DO_NOT_TRACK=1
EOF
Actual:
--- ALL UNSET (control) ---
--- DISABLE_TELEMETRY=0 ---
- Feature-flag evaluation disabled (disabled by DISABLE_TELEMETRY)
--- DISABLE_TELEMETRY=1 ---
- Feature-flag evaluation disabled (disabled by DISABLE_TELEMETRY)
--- NONESSENTIAL=0 ---
- Feature-flag evaluation disabled (disabled by CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)
--- NONESSENTIAL=1 ---
- Feature-flag evaluation disabled (disabled by CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)
--- DO_NOT_TRACK=0 ---
--- DO_NOT_TRACK=1 ---
- Feature-flag evaluation disabled (disabled by DO_NOT_TRACK)
=0 is indistinguishable from =1, and differs from unset. DO_NOT_TRACK shows the correct behavior: =0 behaves like unset.
Root cause
Symbol names below are from the minified 2.1.223 bundle and will differ in source. The bundle ships a boolean parser that handles "0" correctly:
function tr(e){ if(!e) return false; if(typeof e==="boolean") return e;
let t=String(e).toLowerCase().trim();
return ["1","true","yes","on"].includes(t) }
There is also a typed env registry whose bool() is transform(e => tr(e)), and all three variables are registered as bool() — the correct parse path exists for each and is bypassed at these call sites. DO_NOT_TRACK, by contrast, is registered as str() and is explicitly wrapped in tr() at both its read sites.
Three adjacent lines in one function, two raw and one wrapped:
if (process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC) return "essential-traffic"; // raw
if (process.env.DISABLE_TELEMETRY) return "no-telemetry"; // raw
if (tr(process.env.DO_NOT_TRACK)) return "no-telemetry"; // correct
Raw reads used as conditions, all registered bool():
| Variable | Raw sites | Schema enum |
|---|---:|---|
| DISABLE_TELEMETRY | 2 | ["0","1"] |
| CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC | 3 | ["0","1"] |
| DISABLE_ERROR_REPORTING | 2 | ["0","1"] |
DISABLE_ERROR_REPORTING appears in an early-return guard and a || chain:
... || tr(process.env.CLAUDE_CODE_USE_MANTLE) || process.env.DISABLE_ERROR_REPORTING || ga()) return;
function Dfu(){ if (process.env.DISABLE_ERROR_REPORTING) return false; ... }
Both branches are taken when it is "0". Note this one is static analysis only — claude doctor does not surface error-reporting state, so it is not covered by the repro above.
Suggested fix
Wrap the seven raw reads in the existing parser, or read through the typed registry so the registered bool() applies:
-if (process.env.DISABLE_TELEMETRY) return "no-telemetry";
+if (tr(process.env.DISABLE_TELEMETRY)) return "no-telemetry";
No schema or docs change needed — this makes the code match the published enum: ["0","1"].
Scope
I resolved all 808 entries in the typed env registry and checked the 300 typed bool/triBool flags for raw process.env reads used as conditions. Nine flags matched; six are false positives (string-prefix comparisons, Boolean() coercion, === void 0 / === "true" checks, or debug-only display). The three above are the real ones.
Flags accessed via the registry's property form are correct even though they look unwrapped, because each property is defined as get: () => parse(process.env[KEY]).
Impact
A single "0" in a fleet-wide managed-settings.json disables telemetry and feature-flag evaluation for every developer while appearing to enable them, with no warning — and "0" is what the published schema says to write. Existing reports (#47558, #58383, #69528, #76748, #73320) all concern the blast radius of deliberately setting =1; this is the inverse and, as far as I can find, unreported. Those issues describe further downstream effects of losing feature-flag evaluation (prompt-cache TTL, model availability); I have not independently verified those, only the flag-evaluation state shown above.