You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Managed-settings interim fail-closed uses an empty allow list ([[]]) and permanently drops user MCP servers that register inside the window — reproduces via the desktop app, on an account with no managed policy #4419
While the CLI resolves managed settings, it installs an interim "deny everything" MCP policy
(managedAllowedMcpServerLists: [[]] — one list, containing nothing). Any user-configured MCP server
that happens to register during that window is rejected with:
[ERROR] Skipping MCP server "<name>": not permitted by enterprise managed allow list
Two things make this a real bug rather than a benign transient:
It fires even when there is no managed policy at all. In my logs the resolution completes in
5–65 ms with source=none, serverFetchFailed=false, and [managedSettings] applied: no bypass restriction in force (managed policy absent). Nothing failed,
and there is no enterprise policy on this account. The deny is purely pre-emptive.
Servers denied during that window are never restored. The "policy relaxed" restore loop replays
only servers captured in policyRemovedConfigs, and the startup deny paths never write there. Those
servers stay dead for the entire life of the session.
Net effect: opening a session while any session is resolving managed settings silently produces a
session with no user MCP servers, permanently. Built-in servers are exempt (the trusted-first-party
check keeps entries whose source === "builtin"), so it presents as "my servers are broken" rather than
"a policy was applied".
Important scope note
The defect is in the CLI, but it is only reachable when the CLI is driven by a client that sets enableManagedSettings: true. The GitHub Copilot desktop app sets it on every session.create and session.resume. A CLI launched by hand does not, so this cannot be reproduced from a bare terminal
invocation — please see the warning at the top of the repro steps before trying.
This is provable from both sides of the interface:
CLI side (app.js, 1.0.79-5): 21 occurrences of enableManagedSettings, and not one assigns it true. Both enableManagedSettings=!0 and enableManagedSettings:!0 are absent from the entire
bundle. The field initialises to !1 and is thereafter only read from the client payload
(this.enableManagedSettings = n.enableManagedSettings ?? !1). Every managed-settings entry point is
gated on it, including refreshManagedSettings(), which opens with if(!this.enableManagedSettings)return;.
App side (copilot-sdk/index.js, app 1.1.6): passes enableManagedSettings: config.enableManagedSettings into both session.create and session.resume.
That is why the same binary produces 257 deny events under the app and 0 when launched directly.
Impact
Sessions silently lose all user MCP tooling. There is no tools_changed notification, no banner, and
no error surfaced in the UI — only an [ERROR] line in a process log the user has no reason to read.
Agents and skills that depend on those servers fail in ways that look like the servers are
misconfigured, so users edit working configuration trying to fix it.
MCP child processes are spawned and then discarded, so the startup cost is paid for no benefit.
Affected Version
Reproduced on CLI payload 1.0.79-5, driven by desktop app 1.1.6 (commit=a9706b2); also observed
under app 1.1.5 (f459745). Windows 11 x64.
The mechanism is present in every payload from 1.0.72 onward. Marker counts taken directly from each
shipped app.js:
CLI payload
managedAllowedMcpServerLists:[[]]
host-wide fan-out to active sessions
managedSettingsResolutionInFlight
1.0.71
absent
0
0
1.0.72
present
0
0
1.0.73
present
0
0
1.0.78-2
present
1
4
1.0.79-5
present
1
4
1.0.79-9
present
1
4
1.0.72 introduced the empty-allow-list interim state, behind a narrow session-local guard.
1.0.78-2 made it routine: it added fan-out to every active session and widened the trigger guard at
the same time (diff in Additional context).
1.0.71 does not contain the mechanism at all and is the basis of the workaround.
Install timeline on this machine, from ~/.copilot/logs/github-app.*.log:
2026-08-04T07:42:36Z Detected Copilot CLI version at startup version=Copilot binary version: 1.0.78-2 source=Some(Bundled)
2026-08-07T15:03:37Z Detected Copilot CLI version at startup version=Copilot binary version: 1.0.79-5 source=Some(Bundled)
The desktop app version is a precondition, not the cause.copilot-sdk/index.js is byte-identical
between app 1.1.5 and 1.1.6 (SHA-256 648CD26BA299…, 449,504 bytes in both), and the app has passed enableManagedSettings since 1.1.0 — so nothing changed on the app side across the window in which the
symptom appeared.
Note on reading versions: copilot.exe --version self-resolves to the newest installed payload and
will misreport which payload is actually running. Use --no-auto-update --version, or inspect the
loaded module path of a live process.
Affected version
CLI payload 1.0.79-5+. See the bottom of description for details on affected version
Steps to reproduce the behavior
⚠️A hand-launched CLI will not reproduce this, and the clean log it produces is not evidence the
build is fine. Running copilot --server --stdio --no-auto-update directly produced 0 [managedSettings] lines and 0 deny events, while the app-spawned same binary in the same hour
produced 823 and 257 respectively. The reason is in the scope note above: such a process is
never sent enableManagedSettings: true, so it never enters the managed-settings path at all — and
that holds even if you authenticate it. Reproduce through the desktop app.
Install the desktop app (1.1.5 or 1.1.6) with a CLI payload of 1.0.78-2 or later.
Sign in with an account that has no enterprise managed policy. Confirm with the process log — a
clean resolution on such an account looks like this:
2026-08-09T15:19:40.754Z [INFO] [managedSettings] self-fetch starting for account https://github.com/<user>
2026-08-09T15:19:40.755Z [INFO] [managedSettings] device MDM: no policy present on this device
2026-08-09T15:19:40.915Z [INFO] [managedSettings] server policy: none for this account (404/empty) from https://github.com
2026-08-09T15:19:40.916Z [INFO] [managedSettings] effective policy resolved: source=none, bypassDisabled=false, serverFetchFailed=false
2026-08-09T15:19:40.916Z [INFO] [managedSettings] applied: no bypass restriction in force (managed policy absent)
Configure 2 or more user MCP servers in ~/.copilot/mcp-config.json. Transport does not matter —
stdio, http and local all fail together. Do not rely on the built-in GitHub server to show the
failure; it is exempt.
Open several app sessions in quick succession, or open a new session while another is still
starting. Opening a single session already issues session.create and then session.resume roughly
2 s later, so one open produces two overlapping option updates; several opens make a collision
near-certain.
The ^\d{4}-… anchor and the length filter are both necessary. Conversation transcripts and
fetched page text are written into these same logs, so an unanchored search matches quoted copies of
the message and overcounts badly. This produced false positives twice during the investigation.
Output — the denial, with the surrounding lifecycle. From ~/.copilot/logs/process-1786241789611-38652.log. Note the ordering and the millisecond deltas: the
skips land inside the fetch, and the fetch succeeds:
2026-08-09T02:16:45.131Z [INFO] [managedSettings] self-fetch starting for account https://github.com/<user>
2026-08-09T02:16:45.136Z [ERROR] Skipping MCP server "ado-skype": not permitted by enterprise managed allow list
2026-08-09T02:16:45.136Z [ERROR] Skipping MCP server "ado-domoreexp": not permitted by enterprise managed allow list
2026-08-09T02:16:45.136Z [ERROR] Skipping MCP server "mcp_windbg": not permitted by enterprise managed allow list
2026-08-09T02:16:45.136Z [ERROR] Skipping MCP server "obsidian-vault-coding-brain": not permitted by enterprise managed allow list
2026-08-09T02:16:45.196Z [INFO] [managedSettings] self-fetch complete for https://github.com/<user>: bypassDisabled=false, serverFetchFailed=false, serverCacheFallback=false, deviceLoadFailed=false
2026-08-09T02:16:45.634Z [WARNING] Failed to refresh persisted MCP tools for mcp_windbg; continuing with cached tools: Error: MCP server "mcp_windbg" is not connected
deny window here = 65 ms; the servers were denied at +5 ms
serverFetchFailed=false — the policy fetch worked
the MCP child processes had already been spawned
(MCP server stderr {"server_name":"ado-domoreexp", ...} at 02:16:45.079), so the work is done and
then thrown away
To see the window itself and every trigger, pull the whole lifecycle:
Output — a tighter variant, in which a server is denied 1 ms after the fetch already completed,
showing the interim policy is not cleared synchronously with completion:
2026-08-09T02:18:14.600Z [INFO] [managedSettings] self-fetch starting for account https://github.com/<user>
2026-08-09T02:18:14.605Z [ERROR] Skipping MCP server "ado-skype": not permitted by enterprise managed allow list
... (ado-domoreexp, mcp_windbg, obsidian-vault-coding-brain, same millisecond)
2026-08-09T02:18:14.619Z [INFO] [managedSettings] self-fetch complete for https://github.com/<user>: ... serverFetchFailed=false ...
2026-08-09T02:18:14.620Z [ERROR] Skipping MCP server "obsidian-vault-coding-brain": not permitted by enterprise managed allow list
Output — additional trigger sources, each of which opens its own deny window:
2026-08-09T02:18:18.068Z [INFO] [managedSettings] self-fetch starting for account device-only
2026-08-09T02:18:18.076Z [INFO] [managedSettings] self-fetch starting for account https://github.com/<user>
2026-08-09T02:18:18.079Z [INFO] [managedSettings] self-fetch result for device-only discarded: account changed while the fetch was in flight
Reproduction rate. Over the surviving log-retention window (2026-08-08T22:46Z – 2026-08-09T02:18Z),
during ordinary multi-session use: 42 skip records in 9 bursts across 2 CLI processes — obsidian-vault-coding-brain ×15, ado-skype ×9, ado-domoreexp ×9, mcp_windbg ×9. The built-in
GitHub server was never skipped. Not every session is affected; which sessions lose servers permanently
is decided by the timing rule described in Additional context.
Expected behavior
On an account with no enterprise managed policy, all servers configured in ~/.copilot/mcp-config.json
should start and remain available for the life of the session. Concretely:
No deny while the policy is merely undetermined. "We have not resolved the policy yet" should not
be encoded as "a policy exists and it permits nothing." With source=none, serverFetchFailed=false
and managed policy absent, zero servers should be filtered.
If a pre-emptive deny is genuinely required, it must be reversible. A server denied during the
interim window should be restored when the window closes, exactly as a server stopped by a real
policy tightening already is. Today the two cases behave differently and only one of them recovers.
Registration should wait for the policy rather than race it. The gate that exists for this purpose
should be in force during startup registration.
A resolution that has not failed should not disturb sessions that are already running. Servers
already connected should not be torn down for a routine re-resolution in another session.
If servers are withheld, say so where the user will see it. The failure is currently silent in the
UI, and the message that does exist blames an enterprise policy that is not present on the account.
The file is bundled one module per line, so the relevant lines are 100–150 KB wide (line 2345 is 147,313
chars; line 2398 is 114,363). A line number alone will not locate anything — use line:col, or the byte
offset. Both are given; either can be re-derived by searching for the symbol name, which is unminified.
#
What
Symbol
line:col
byte offset
1
Producer emits the empty allow list
composedManagedMcpHostOptions
2398:44941
2147203
1a
The literal itself
managedAllowedMcpServerLists:[[]]
2398:45016
2147278
2
Consumer's length > 0 test
managedMcpPolicyPlan
2345:75295
1893804
3
Tighten + relax, both halves
enforceManagedMcpPolicy
2345:76770
1895279
3a
The only .set() in the bundle
policyRemovedConfigs.set(
2345:77656
1896165
4
Deny path A — bulk start loop
after managedMcpPolicyBlockReasonForBulk
2345:79639
1898148
5
Deny path B — first check
startServerOnce
2345:93949
1912458
6
Deny path C — second check
startServerOnce
2345:94664
1913173
7
The gate that opts itself out
expectManagedMcpPolicy
2398:43337
2145599
8
Host-wide fan-out
beginManagedSettingsResolution
2398:42842
2145104
9
The trigger
updateOptions
2402:67610
2284349
10
Client-supplied flag default
enableManagedSettings ?? !1
2402:40113
2256852
To re-derive line:col from an offset ([regex]::Matches over a 9 MB string hangs — use IndexOf):
There is a fourth mcpHostStateAddFiltered(...) call at 2345:93083 / offset 1911592, inside applyConfigFilterToServer. It is not part of this defect — it is the separate config-filter path
and logs MCP server "X" filtered: <reason>, not the allow-list message. Noted so the count is not
mistaken for four managed-policy deny sites; there are three.
Root cause
1. The producer emits "a policy exists and it allows nothing"
Note the asymmetry with the normal path directly below it: when there is genuinely no policy the normal
path returns void 0, but the interim path returns [[]].
2. The consumer's "is a policy configured?" test cannot tell [[]] from a real policy
[[]] is truthy for length > 0, so the native policy engine runs against an empty allow list and
removes everything not exempt. The user-visible string comes from o.logMessages[0], produced by the
native addon — which is why grepping app.js for not permitted by enterprise managed allow list finds
nothing.
Exemption set — why built-ins survive and every user server dies together regardless of transport:
isRegisteredServerTrustedFirstParty(e){returnthis.trustedDynamicStartServerNames.has(e)||this.trustedDefaultServerNames.includes(e)||this.trustedExternalServerNames.has(e);}// trustedDefaultServerNames keeps only entries whose source === "builtin"
3. The bug's teeth: the startup deny paths are not restorable
enforceManagedMcpPolicy contains both halves of the policy transition. On tighten it stops the server and stashes its config; on relax it replays only what was stashed:
// tighten: stop + stashtry{awaitthis.stopServer(l);}catch(d){/* ... */}this.policyRemovedConfigs.set(l,c);// <-- the ONLY .set() in the file (2345:77656)deletethis.config.mcpServers[l];// relax: restorefor(let[l,c]of[...this.policyRemovedConfigs])if(!s.has(l)){if(this.config.mcpServers[l]){this.policyRemovedConfigs.delete(l);continue;}try{awaitthis.startServer(l,c);}catch(d){/* ... */continue;}this.config.mcpServers[l]&&(this.policyRemovedConfigs.delete(l),a=!0);}
But a server denied while starting never reaches that stash. startServerOnce checks the policy
twice and both times just marks the server filtered and returns:
asyncstartServerOnce(e,n,r,o,s=!1){leta=this.currentServerStopVersion(e);if(!this.validateServerConfig(e,n))return;letl=awaitthis.managedMcpPolicyBlockReason(e,n,s);if(l){h.mcpHostStateAddFiltered(this.hostStateHandle,e);this.logger.log(l);return;}// 2345:93949/* ... gate plan, config filtering ... */letg=awaitthis.managedMcpPolicyBlockReason(e,d,s);if(g||this.currentServerStopVersion(e)!==a){g&&(h.mcpHostStateAddFiltered(this.hostStateHandle,e),this.logger.log(g));return;// 2345:94664}this.config.mcpServers[e]=d;// only reached when NOT blocked/* ... connect ... */}
and the bulk path additionally deletes the config outright:
letd=awaitthis.managedMcpPolicyBlockReasonForBulk(l,c);if(d){this.logger.log(d);h.mcpHostStateAddFiltered(this.hostStateHandle,l);deletethis.config.mcpServers[l];// <-- dropped, never stashed for restore (2345:79639)return;}
There is exactly one policyRemovedConfigs.set(...) in the bundle, and none of the three
managed-policy deny paths is it. Therefore:
when the window opens
outcome
server already registered
stopped, stashed, restored automatically when the policy clears
server registering right now
filtered + config dropped, never restored — dead for the life of the session
A fresh session registers its MCP servers at session start, which is exactly when the window opens, so
the common case is the non-restorable one. This single asymmetry is why the failure looks
non-deterministic, and why "it fixed itself after a minute" and "it was dead for the whole session" are
both accurate reports of the same defect.
Servers in the second row can still come back if a later full MCP client rebuild happens to re-register
them from the original config — the teardown/rebuild described in #4392. That is incidental rather than
the restore path, and it can be denied again: I measured 4 consecutive bursts on the same 4 servers
within 90 s.
4. What opens the window, and why it opens so often
beginManagedSettingsResolution(){if(this.lastManagedSettingsLayers){this.managedSettingsResolutionInFlight=!0;this.managedSettingsPolicyGeneration++;for(letnofthis.activeSessions.values())n.beginManagedSettingsResolution({permissions: !1});// fan-out to EVERY live session}}
{permissions:false} skips the permissions half, but the session-level handler still always calls beginInterimManagedMcpFailClosed(). A session that joins while a resolution is in flight is
immediately fail-closed too:
There is a gate intended to make MCP registration wait for the policy, but it opts itself out whenever
the host already exists, so registration proceeds against the interim [[]]:
expectManagedMcpPolicy(){if(this.managedMcpPolicyApplied||this.mcpHost||this.managedMcpPolicyGate)return;/* ...create gate... */}// consumer:this.managedMcpPolicyGate&&awaitthis.managedMcpPolicyGate.promise;// no gate -> awaits nothing
The 1.0.78-2 trigger-guard change
// 1.0.73 — narrow: authInfo present AND identity defined AND identity changed; session-local effectif(e.authInfo!==void0&&this.enableManagedSettings){letl=YX(this.authInfo);l!==void0&&l!==this.managedSettingsFetchIdentity&&(this.managedSettingsFetchIdentity=l,this.beginInterimManagedMcpFailClosed(),// this session only/* ... */);}// 1.0.78-2 and later — broad: key merely present, undefined identity no longer suppresses,// plus a generation check; effect is now host-wideif(this.enableManagedSettings&&("authInfo"ine||e.enableManagedSettings===!0)){letc=ene(this.authInfo);(c!==this.managedSettingsFetchIdentity||this.managedSettingsAuthGeneration!==this.managedSettingsFetchGeneration)&&(this.managedSettingsFetchIdentity=c,this.managedSettingsFetchGeneration=this.managedSettingsAuthGeneration,this.beginManagedSettingsResolution(),// fan-out to ALL sessions/* ... */);}
Three widenings at once: e.authInfo !== void 0 became "authInfo" in e; the l !== void 0 guard was
dropped; and a generation disjunct was added. managedSettingsAuthGeneration++ (new in 1.0.78-2, fired
on auth_info_updated) means each auth refresh arms the next options update to re-trigger the cycle.
The hourlyrefreshManagedSettings() timer does not call beginManagedSettingsResolution() — only updateOptions does. So the window is tied to session churn, not to the timer.
Suggested fixes (any one breaks the chain)
Do not use [[]] as a sentinel. Represent "policy undetermined" as a distinct flag, or make the
consumer test r.some(list => list.length > 0) instead of r.length > 0.
Make the deny paths restorable — have the startServerOnce and bulk deny paths record the config
into policyRemovedConfigs (or an equivalent) so the "policy relaxed" loop can replay them. This is
the smallest change that removes the permanent-loss case.
Fix the gate — drop || this.mcpHost from expectManagedMcpPolicy() so registration waits for
the resolution instead of racing it.
Do not fan out to already-running sessions, or at minimum do not tear down servers that are
already connected for a resolution that has not failed.
Workaround (verified)
Pin the CLI to 1.0.71, which does not contain the mechanism at all:
[Environment]::SetEnvironmentVariable(
'COPILOT_CLI_PATH',"$env:LOCALAPPDATA\github-copilot-sdk\cli\1.0.71\copilot.exe",'User')
# then fully exit and restart the app
Confirmed applied — note that the reported version string is unreliable here; the path is ground truth:
2026-08-09T05:01:55Z Detected Copilot CLI version at startup version=GitHub Copilot CLI 1.0.79-9
source=Some(Local) path="...\github-copilot-sdk\cli\1.0.71\copilot.exe"
bundled_version="1.0.79-5"
All 8 running copilot.exe processes load their payload from pkg\win32-x64\1.0.71\, and there have
been zero deny events in the ~11 h since, across continued multi-session use.
What we could not determine
Whether the interim state is ever legitimately needed. If the intent is to fail closed only when
the policy genuinely cannot be determined, then the serverFetchFailed=false / managed policy absent
case observed here should never have entered it at all.
Exact first occurrence.process-*.log files rotate; the oldest surviving one starts
2026-08-08T22:24Z. Earlier deny events were observed during this investigation but those files are now
gone. The version matrix is the reliable dating, not the logs.
Why the perceived onset was 2026-08-08 when 1.0.78-2 shipped on 08-04. Best explanation: the
fan-out only bites when sessions overlap, and concurrent multi-session usage began on 08-08. Not
proven — offered as a hypothesis, not a finding.
Please do not close this as a duplicate of the fetch-failure family. The discriminator is one field
in the resolved line: every filed variant has serverFetchFailed=true; this one has serverFetchFailed=false with source=none / managed policy absent. Relatedly, the v1.0.78 release
note advertises a fail-closed → fail-open change, but that covers the fetch-failure path only. This
variant is untouched by it and in fact reproduces on 1.0.79-5, which postdates it.
A third-party report quoting Skipping MCP server "umacs-build-bridge" dated 2026-08-03 matches the
1.0.72/1.0.73 era, consistent with the mechanism landing in 1.0.72.
Describe the bug
While the CLI resolves managed settings, it installs an interim "deny everything" MCP policy
(
managedAllowedMcpServerLists: [[]]— one list, containing nothing). Any user-configured MCP serverthat happens to register during that window is rejected with:
Two things make this a real bug rather than a benign transient:
5–65 ms with
source=none,serverFetchFailed=false, and[managedSettings] applied: no bypass restriction in force (managed policy absent). Nothing failed,and there is no enterprise policy on this account. The deny is purely pre-emptive.
only servers captured in
policyRemovedConfigs, and the startup deny paths never write there. Thoseservers stay dead for the entire life of the session.
Net effect: opening a session while any session is resolving managed settings silently produces a
session with no user MCP servers, permanently. Built-in servers are exempt (the trusted-first-party
check keeps entries whose
source === "builtin"), so it presents as "my servers are broken" rather than"a policy was applied".
Important scope note
The defect is in the CLI, but it is only reachable when the CLI is driven by a client that sets
enableManagedSettings: true. The GitHub Copilot desktop app sets it on everysession.createandsession.resume. A CLI launched by hand does not, so this cannot be reproduced from a bare terminalinvocation — please see the warning at the top of the repro steps before trying.
This is provable from both sides of the interface:
app.js, 1.0.79-5): 21 occurrences ofenableManagedSettings, and not one assigns ittrue. BothenableManagedSettings=!0andenableManagedSettings:!0are absent from the entirebundle. The field initialises to
!1and is thereafter only read from the client payload(
this.enableManagedSettings = n.enableManagedSettings ?? !1). Every managed-settings entry point isgated on it, including
refreshManagedSettings(), which opens withif(!this.enableManagedSettings)return;.copilot-sdk/index.js, app 1.1.6): passesenableManagedSettings: config.enableManagedSettingsinto bothsession.createandsession.resume.That is why the same binary produces 257 deny events under the app and 0 when launched directly.
Impact
tools_changednotification, no banner, andno error surfaced in the UI — only an
[ERROR]line in a process log the user has no reason to read.enterprise managed allow list) that does not exist on the account,sending users to hunt for a nonexistent policy. At least one upstream reporter (Post-authentication MCP client rebuild at startup leaves orphaned stdio MCP server processes #4392) read it as a
genuine corporate whitelist.
misconfigured, so users edit working configuration trying to fix it.
Affected Version
Reproduced on CLI payload 1.0.79-5, driven by desktop app 1.1.6 (
commit=a9706b2); also observedunder app 1.1.5 (
f459745). Windows 11 x64.The mechanism is present in every payload from 1.0.72 onward. Marker counts taken directly from each
shipped
app.js:managedAllowedMcpServerLists:[[]]managedSettingsResolutionInFlightthe same time (diff in Additional context).
Install timeline on this machine, from
~/.copilot/logs/github-app.*.log:The desktop app version is a precondition, not the cause.
copilot-sdk/index.jsis byte-identicalbetween app 1.1.5 and 1.1.6 (SHA-256
648CD26BA299…, 449,504 bytes in both), and the app has passedenableManagedSettingssince 1.1.0 — so nothing changed on the app side across the window in which thesymptom appeared.
Affected version
CLI payload 1.0.79-5+. See the bottom of description for details on affected version
Steps to reproduce the behavior
clean resolution on such an account looks like this:
~/.copilot/mcp-config.json. Transport does not matter —stdio, http and local all fail together. Do not rely on the built-in GitHub server to show the
failure; it is exempt.
starting. Opening a single session already issues
session.createand thensession.resumeroughly2 s later, so one open produces two overlapping option updates; several opens make a collision
near-certain.
Output — the denial, with the surrounding lifecycle. From
~/.copilot/logs/process-1786241789611-38652.log. Note the ordering and the millisecond deltas: theskips land inside the fetch, and the fetch succeeds:
serverFetchFailed=false— the policy fetch worked(
MCP server stderr {"server_name":"ado-domoreexp", ...}at02:16:45.079), so the work is done andthen thrown away
Output — a tighter variant, in which a server is denied 1 ms after the fetch already completed,
showing the interim policy is not cleared synchronously with completion:
Output — additional trigger sources, each of which opens its own deny window:
Reproduction rate. Over the surviving log-retention window (2026-08-08T22:46Z – 2026-08-09T02:18Z),
during ordinary multi-session use: 42 skip records in 9 bursts across 2 CLI processes —
obsidian-vault-coding-brain×15,ado-skype×9,ado-domoreexp×9,mcp_windbg×9. The built-inGitHub server was never skipped. Not every session is affected; which sessions lose servers permanently
is decided by the timing rule described in Additional context.
Expected behavior
On an account with no enterprise managed policy, all servers configured in
~/.copilot/mcp-config.jsonshould start and remain available for the life of the session. Concretely:
be encoded as "a policy exists and it permits nothing." With
source=none,serverFetchFailed=falseand
managed policy absent, zero servers should be filtered.interim window should be restored when the window closes, exactly as a server stopped by a real
policy tightening already is. Today the two cases behave differently and only one of them recovers.
should be in force during startup registration.
already connected should not be torn down for a routine re-resolution in another session.
UI, and the message that does exist blames an enterprise policy that is not present on the account.
Additional context
Where the code is
All references are to the shipped bundle:
The file is bundled one module per line, so the relevant lines are 100–150 KB wide (line 2345 is 147,313
chars; line 2398 is 114,363). A line number alone will not locate anything — use line:col, or the byte
offset. Both are given; either can be re-derived by searching for the symbol name, which is unminified.
composedManagedMcpHostOptionsmanagedAllowedMcpServerLists:[[]]length > 0testmanagedMcpPolicyPlanenforceManagedMcpPolicy.set()in the bundlepolicyRemovedConfigs.set(managedMcpPolicyBlockReasonForBulkstartServerOncestartServerOnceexpectManagedMcpPolicybeginManagedSettingsResolutionupdateOptionsenableManagedSettings ?? !1To re-derive line:col from an offset (
[regex]::Matchesover a 9 MB string hangs — useIndexOf):There is a fourth
mcpHostStateAddFiltered(...)call at 2345:93083 / offset 1911592, insideapplyConfigFilterToServer. It is not part of this defect — it is the separate config-filter pathand logs
MCP server "X" filtered: <reason>, not the allow-list message. Noted so the count is notmistaken for four managed-policy deny sites; there are three.
Root cause
1. The producer emits "a policy exists and it allows nothing"
Note the asymmetry with the normal path directly below it: when there is genuinely no policy the normal
path returns
void 0, but the interim path returns[[]].2. The consumer's "is a policy configured?" test cannot tell
[[]]from a real policy[[]]is truthy forlength > 0, so the native policy engine runs against an empty allow list andremoves everything not exempt. The user-visible string comes from
o.logMessages[0], produced by thenative addon — which is why grepping
app.jsfornot permitted by enterprise managed allow listfindsnothing.
Exemption set — why built-ins survive and every user server dies together regardless of transport:
3. The bug's teeth: the startup deny paths are not restorable
enforceManagedMcpPolicycontains both halves of the policy transition. On tighten it stops the serverand stashes its config; on relax it replays only what was stashed:
But a server denied while starting never reaches that stash.
startServerOncechecks the policytwice and both times just marks the server filtered and returns:
and the bulk path additionally deletes the config outright:
There is exactly one
policyRemovedConfigs.set(...)in the bundle, and none of the threemanaged-policy deny paths is it. Therefore:
A fresh session registers its MCP servers at session start, which is exactly when the window opens, so
the common case is the non-restorable one. This single asymmetry is why the failure looks
non-deterministic, and why "it fixed itself after a minute" and "it was dead for the whole session" are
both accurate reports of the same defect.
Servers in the second row can still come back if a later full MCP client rebuild happens to re-register
them from the original config — the teardown/rebuild described in #4392. That is incidental rather than
the restore path, and it can be denied again: I measured 4 consecutive bursts on the same 4 servers
within 90 s.
4. What opens the window, and why it opens so often
{permissions:false}skips the permissions half, but the session-level handler still always callsbeginInterimManagedMcpFailClosed(). A session that joins while a resolution is in flight isimmediately fail-closed too:
There is a gate intended to make MCP registration wait for the policy, but it opts itself out whenever
the host already exists, so registration proceeds against the interim
[[]]:The 1.0.78-2 trigger-guard change
Three widenings at once:
e.authInfo !== void 0became"authInfo" in e; thel !== void 0guard wasdropped; and a generation disjunct was added.
managedSettingsAuthGeneration++(new in 1.0.78-2, firedon
auth_info_updated) means each auth refresh arms the next options update to re-trigger the cycle.The hourly
refreshManagedSettings()timer does not callbeginManagedSettingsResolution()— onlyupdateOptionsdoes. So the window is tied to session churn, not to the timer.Suggested fixes (any one breaks the chain)
[[]]as a sentinel. Represent "policy undetermined" as a distinct flag, or make theconsumer test
r.some(list => list.length > 0)instead ofr.length > 0.startServerOnceand bulk deny paths record the configinto
policyRemovedConfigs(or an equivalent) so the "policy relaxed" loop can replay them. This isthe smallest change that removes the permanent-loss case.
|| this.mcpHostfromexpectManagedMcpPolicy()so registration waits forthe resolution instead of racing it.
already connected for a resolution that has not failed.
Workaround (verified)
Pin the CLI to 1.0.71, which does not contain the mechanism at all:
Confirmed applied — note that the reported version string is unreliable here; the path is ground truth:
All 8 running
copilot.exeprocesses load their payload frompkg\win32-x64\1.0.71\, and there havebeen zero deny events in the ~11 h since, across continued multi-session use.
What we could not determine
the policy genuinely cannot be determined, then the
serverFetchFailed=false/managed policy absentcase observed here should never have entered it at all.
process-*.logfiles rotate; the oldest surviving one starts2026-08-08T22:24Z. Earlier deny events were observed during this investigation but those files are now
gone. The version matrix is the reliable dating, not the logs.
fan-out only bites when sessions overlap, and concurrent multi-session usage began on 08-08. Not
proven — offered as a hypothesis, not a finding.
Related issues
user-configured MCP servers" — same symptom, different cause (fetch fails). Here the fetch
succeeds and there is no policy, so this is a distinct defect that Cloud agent: MCP registry policy fetch fails with 401/403 on GHEC data residency, silently blocking all user-configured MCP servers #4378's fix would not cover.
input-handling variant. Also fetch-side.
behind the observed silent recoveries, and its author also read the allow-list message as a real
corporate policy.
Please do not close this as a duplicate of the fetch-failure family. The discriminator is one field
in the resolved line: every filed variant has
serverFetchFailed=true; this one hasserverFetchFailed=falsewithsource=none/managed policy absent. Relatedly, the v1.0.78 releasenote advertises a fail-closed → fail-open change, but that covers the fetch-failure path only. This
variant is untouched by it and in fact reproduces on 1.0.79-5, which postdates it.
A third-party report quoting
Skipping MCP server "umacs-build-bridge"dated 2026-08-03 matches the1.0.72/1.0.73 era, consistent with the mechanism landing in 1.0.72.