On the morning of September 10 I opened Claude Code and found a notice I hadn't seen before: the organization's configuration used fields that are still accepted now but won't be for much longer. The Details dialog listed each field, its replacement, and the date it stops being accepted.
My first reading was that this was a spelling cleanup. If the names were simply changing, I figured I could do them all in one sitting sometime in October.
I had it backwards.
What happens on October 7 is not being ignored — it's falling back
The cut-off is October 7, 2026, 12:00 PM Pacific Time. The warning machinery shipped in the August 27 build, and per-user display began on September 10.
The part that's easy to misread is what happens afterward. If the old names were simply skipped over and everything kept working, then being late to fix them would cost almost nothing. That isn't the behavior. After the cut-off, a renamed key's old name falls back to its fail-closed value or its default.
Say you've been shipping trustBootstrapLocalExec: true to your fleet. On October 7 that true stops arriving. The drift is toward stricter, not looser. And if you have an invalid entry under managedMcpServers or orgPluginSettings, that connector or tool policy becomes unavailable until someone rewrites it.
So what changes on October 7 isn't how readable your config file is. It's what your defaults are from that day forward.
A deprecation with a deadline is not a naming problem. It's a defaults problem. Getting that backwards is exactly why I filed it one notch lower than it deserved.
One detail about the warning itself: disableConfigDeprecationWarnings suppresses the first showing, but the final reminder in the 24 hours before the cut-off still appears. Suppression only covers the opening notice — it doesn't make the deadline go away.
The old-to-new map, written down once
The mapping is in the Details dialog, but having a copy at hand makes the migration go faster. Watch for the rows where the value or the shape changes rather than the name.
| Old spelling | Replacement | Note |
|---|---|---|
inferenceGatewayHeaders | inferenceCustomHeaders | The value must be a JSON object too |
trustBootstrapLocalExec | trustBootstrapDelivery | — |
enduserAttribution | endUserAttribution | Only the capital E changes |
isDxtEnabled | isDesktopExtensionEnabled | — |
isDxtSignatureRequired | isDesktopExtensionSignatureRequired | — |
inferenceGatewayAuthScheme: "sso" | inferenceCredentialKind: "interactive" | The value side changes |
inferenceGatewayAuthScheme: "auto" | Remove the key | bearer is the default |
| Header maps as strings or lists | A JSON object | Applies to otlpHeaders, otlpResourceAttributes, bootstrapHeaders |
orgPluginSettings record form | The array form | Builds before 1.15200.0 read only the record form |
Tool permission "ask-session" | "ask" | Applies to builtinToolPolicy and friends |
managedMcpServers[].scopes | scope | One space-separated string |
oauth as a number or string | true or an oauth object | — |
The orgPluginSettings row is the one that doesn't migrate cleanly in a single direction. Moving to the new form makes the config unreadable to older clients, so you need to confirm which builds are actually deployed before you touch it. It's the only row where the deadline and the version floor collide.
A window where neither path is read
Separate from the October 7 batch, the September 4 build moved one key: in served configuration, relaunchEnforcementHours moved out from under bootstrap and into lifecycle.
The awkward part is the read direction. Builds from September 4 onward no longer read the old path, and earlier builds do not read the new one. If your fleet isn't on a single version and you move the value, you open a window where neither side reads it.
During that window the value doesn't vanish — it reverts to the default. And in the same build, the default grace period before a required restart went from 1 hour to 24. That's a combination where you can go a while without noticing the value isn't landing.
There's one more layer: if a device-management profile sets any app-behavior key, that profile takes precedence over the served value for this key. Setting it only on the served side won't take effect. I nearly skipped past that line and spent a while moving the setting back and forth on my own machine before I reread it.
Audit the config with a check that can actually fail
There are too many items to catch by eye, and some of them sit several levels down in the nesting. I wrote the following script and started running config files through it. It picks up renamed keys, renamed values, shape violations, and the relocated path in one pass.
#!/usr/bin/env python3
"""Find spellings in managed settings that stop being accepted on 2026-10-07."""
import json, sys, pathlib
RENAMES = { # old -> (new, note)
"inferenceGatewayHeaders": ("inferenceCustomHeaders", "value must be a JSON object"),
"trustBootstrapLocalExec": ("trustBootstrapDelivery", ""),
"enduserAttribution": ("endUserAttribution", "capital E"),
"isDxtEnabled": ("isDesktopExtensionEnabled", ""),
"isDxtSignatureRequired": ("isDesktopExtensionSignatureRequired", ""),
}
VALUE_RENAMES = { # the value itself is the deprecated part
"inferenceGatewayAuthScheme": {
"sso": 'replace with inferenceCredentialKind: "interactive"',
"auto": "drop the key (bearer is the default)",
},
}
OBJECT_ONLY = ("inferenceCustomHeaders", "inferenceGatewayHeaders",
"otlpHeaders", "otlpResourceAttributes", "bootstrapHeaders")
TOOL_POLICY_KEYS = ("builtinToolPolicy", "managedMcpServers", "orgPluginSettings")
def walk(node, path=""):
"""Yield (path, key, value) for every node, however deeply nested."""
if isinstance(node, dict):
for k, v in node.items():
here = f"{path}.{k}" if path else k
yield here, k, v
yield from walk(v, here)
elif isinstance(node, list):
for i, v in enumerate(node):
yield from walk(v, f"{path}[{i}]")
def audit(cfg):
hits = []
for path, key, value in walk(cfg):
if key in RENAMES:
new, note = RENAMES[key]
hits.append((path, f"{key} -> {new}", note))
if key in VALUE_RENAMES and isinstance(value, str):
note = VALUE_RENAMES[key].get(value)
if note:
hits.append((path, f'{key}: "{value}"', note))
if key in OBJECT_ONLY and not isinstance(value, dict) and key not in RENAMES:
hits.append((path, f"{key} is a {type(value).__name__}",
"rewrite it as a JSON object"))
if key == "orgPluginSettings" and isinstance(value, dict):
hits.append((path, "orgPluginSettings is in record form",
"rewrite it as an array"))
if key == "scopes" and "managedMcpServers" in path:
hits.append((path, "managedMcpServers[].scopes",
"use scope: one space-separated string"))
if key == "oauth" and not isinstance(value, (bool, dict)):
hits.append((path, f"oauth is a {type(value).__name__}",
"use true or an oauth object"))
if value == "ask-session" and any(t in path for t in TOOL_POLICY_KEYS):
hits.append((path, '"ask-session"', 'use "ask"'))
# Not part of the 10/7 batch: the path that moved on 2026-09-04
if isinstance(cfg.get("bootstrap"), dict) and "relaunchEnforcementHours" in cfg["bootstrap"]:
hits.append(("bootstrap.relaunchEnforcementHours",
"moved to lifecycle.relaunchEnforcementHours",
"builds from 2026-09-04 do not read the old path"))
return hits
def main(paths):
total = 0
for p in paths:
f = pathlib.Path(p)
if not f.exists():
print(f"[skip] {p} (not found)")
continue
try:
cfg = json.loads(f.read_text())
except json.JSONDecodeError as e:
# Since September 4, an unparseable config blocks startup entirely
print(f"[FATAL] {p}: not valid JSON -> {e}")
total += 1
continue
hits = audit(cfg)
print(f"\n=== {p} — {len(hits)} to fix ===")
for path, what, note in hits:
print(f" {path}\n {what}" + (f"\n {note}" if note else ""))
total += len(hits)
print(f"\n{total} total")
return 1 if total else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:] or ["managed-settings.json"]))Run it against a file deliberately stuffed with old spellings and you get:
=== sample-managed-settings.json — 11 to fix ===
inferenceGatewayHeaders
inferenceGatewayHeaders -> inferenceCustomHeaders
value must be a JSON object
inferenceGatewayAuthScheme
inferenceGatewayAuthScheme: "sso"
replace with inferenceCredentialKind: "interactive"
...
managedMcpServers[0].scopes
managedMcpServers[].scopes
use scope: one space-separated string
bootstrap.relaunchEnforcementHours
moved to lifecycle.relaunchEnforcementHours
builds from 2026-09-04 do not read the old path
11 totalRun the rewritten config through the same script and you get zero hits and an exit code of 0. That exit code is the whole point. A check that can't fail the build is decoration, not a check. I've written the eyeball-the-output kind before, and it took me a long time to notice it had never caught anything.
Separately from deprecation, a key name you simply misspell is still swallowed without a word. I wrote that one up in A One-Letter Typo in settings.json Is Ignored Without a Single Warning. Reading the two together makes it clearer which parts of your config are validated at all — and which parts you're simply trusting.
A broken config now blocks startup
One last change, and it sits underneath the migration work itself. Since the September 4 build, if a device's managed-settings.json, one of its drop-ins, the device-management plist, or the Windows policy registry value cannot be parsed, Claude Code refuses to start and names the source.
It used to ignore the unreadable source and start anyway. That was friendlier, and it also meant you could go indefinitely without knowing a setting wasn't taking effect. Now it stops.
Which means a single trailing comma left behind mid-migration will keep sessions on that machine from starting at all. That's why the script above bails out with [FATAL] the moment parsing fails rather than continuing:
[FATAL] broken.json: not valid JSON -> Expecting property name enclosed in double quotes: line 1 column 26 (char 25)
1 totalBefore pushing a config change out to machines, I run it through this step every time. The difference in effort between a failure you catch on one machine and a failure you catch after distribution is not small.
Where to start
Run your own managed-settings.json through the script once and look at the count. If it's zero, you're done thinking about October 7. If anything comes back, start by working out which default that particular line falls back to after the cut-off — that tells you how urgent it actually is.
If you want to make the audit a standing habit rather than a one-off, Until I planted a failing sample, my unattended checks had never once failed goes through building checks that are verified to fail, sample and all.
Deadlines have a way of turning into "I'll batch it later." Mine did. Thanks for reading.