
Add Policy
- 190 installs
- 188k repo stars
- Updated July 28, 2026
- microsoft/vscode
Use when adding, modifying, or reviewing VS Code configuration policies.
About
Use when adding, modifying, or reviewing VS Code configuration policies. Covers the full policy lifecycle from registration to export to platform-specific artifacts. Run on ANY change that adds a `policy:` field to a configuration property. Policies allow enterprise administrators to lock configuration settings via OS-level mechanisms (Windows Group Policy, macOS managed preferences, Linux config files) or via Copilot account-level policy data. This skill covers the complete procedure.
- # Adding a Configuration Policy
- Adding a new `policy:` field to any configuration property
- Modifying an existing policy (rename, category change, etc.)
- Reviewing a PR that touches policy registration
- Adding account-based policy support via `IPolicyData`
Add Policy by the numbers
- 190 all-time installs (skills.sh)
- Ranked #819 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
add-policy capabilities & compatibility
- Capabilities
- # adding a configuration policy · adding a new `policy:` field to any configuratio · modifying an existing policy (rename, category c · reviewing a pr that touches policy registration
- Use cases
- documentation
What add-policy says it does
Use when adding, modifying, or reviewing VS Code configuration policies. Covers the full policy lifecycle from registration to export to platform-specific artifacts. Run on ANY change that adds a `pol
npx skills add https://github.com/microsoft/vscode --skill add-policyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 190 |
|---|---|
| repo stars | ★ 188k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | microsoft/vscode ↗ |
How do I apply add-policy using the workflow in its SKILL.md?
Use when adding, modifying, or reviewing VS Code configuration policies. Covers the full policy lifecycle from registration to export to platform-specific artifacts. Run on ANY change tha...
Who is it for?
Developers following the add-policy skill for the tasks it documents.
Skip if: Tasks outside the add-policy scope described in SKILL.md.
When should I use this skill?
User mentions add-policy or related triggers from the skill description.
What you get
Working add-policy setup aligned with the documented patterns and constraints.
Files
Adding a Configuration Policy
Policies allow enterprise administrators to lock configuration settings via OS-level mechanisms (Windows Group Policy, macOS managed preferences, Linux config files) or via Copilot account-level policy data. This skill covers the complete procedure.
When to Use
- Adding a new
policy:field to any configuration property - Modifying an existing policy (rename, category change, etc.)
- Reviewing a PR that touches policy registration
- Adding account-based policy support via
IPolicyData - Wiring an enterprise managed setting (native MDM / GitHub server) — see [github-managed-settings.md](./github-managed-settings.md)
- Having one policy govern multiple settings via
policyReference - Testing account/managed-settings policies locally without the real backend — see [local-testing.md](./local-testing.md)
Architecture Overview
Policy Sources (layered, last writer wins)
| Source | Implementation | How it reads policies |
|---|---|---|
| OS-level (Windows registry, macOS plist) | NativePolicyService via @vscode/policy-watcher | Watches Software\Policies\Microsoft\{productName} (Windows) or bundle identifier prefs (macOS) |
| Linux file | FilePolicyService | Reads /etc/vscode/policy.json |
| Account/GitHub | AccountPolicyService | Reads IPolicyData from IDefaultAccountService.policyData, applies value() function. Server-delivered managed settings arrive on policyData.managedSettings; native MDM is a separate input (ICopilotManagedSettingsService) that AccountPolicyService selects between in getPolicyData() (server wins when present; no merging between layers) |
| Copilot managed settings (native MDM) | CopilotManagedSettingsService via @vscode/policy-watcher | Watches SOFTWARE\Policies\GitHubCopilot (Windows) / com.github.copilot prefs (macOS); feeds the canonical managedSettings bag — see github-managed-settings.md |
| Multiplex | MultiplexPolicyService | In the main process, combines multiple OS/file policy readers; in desktop and Agents-window renderers, combines the main-process PolicyChannelClient with AccountPolicyService |
Key Files
| File | Purpose |
|---|---|
src/vs/base/common/policy.ts | PolicyCategory enum, IPolicy interface, IPolicyReference, ManagedSettingsData, IManagedSettingsPolicyDefinitions |
src/vs/platform/policy/common/policy.ts | IPolicyService, AbstractPolicyService, PolicyDefinition, toSerializablePolicyDefinition (drops the non-cloneable value() for IPC), getRestrictedPolicyValue |
src/vs/platform/policy/common/copilotManagedSettings.ts | Managed-settings key constants, collectManagedSettingsDefinitions, projectManagedSettings, flattenManagedSettings, ICopilotManagedSettingsService |
src/vs/platform/policy/node/copilotManagedSettingsService.ts | Native MDM watcher (@vscode/policy-watcher) for Copilot managed settings |
src/vs/platform/configuration/common/configurations.ts | PolicyConfiguration — bridges policies to configuration values; parses JSON-string managed settings back to typed values; applies values to policyReference settings |
src/vs/platform/configuration/common/configurationRegistry.ts | policy / policyReference registration; getPolicyReferenceConfigurations() (name → subordinate settings) |
src/vs/workbench/services/policies/common/accountPolicyService.ts | Account/GitHub-based policy evaluation; selects + projects managed settings (server over MDM; single authoritative layer) |
src/vs/workbench/services/accounts/browser/managedSettings.ts | adaptManagedSettings — normalizes the server managed_settings response into the canonical bag |
src/vs/workbench/services/policies/common/multiplexPolicyService.ts | Combines multiple policy services |
src/vs/workbench/contrib/policyExport/electron-browser/policyExport.contribution.ts | --export-policy-data CLI handler |
src/vs/base/common/defaultAccount.ts | IPolicyData interface (incl. managedSettings) for account-level policy fields |
build/lib/policies/policyData.jsonc | Auto-generated policy catalog incl. referencedSettings (DO NOT edit manually) |
build/lib/policies/policyGenerator.ts | Generates ADMX/ADML (Windows), plist (macOS), JSON (Linux) |
build/lib/test/policyConversion.test.ts | Tests for policy artifact generation |
Procedure
Step 1 — Add the policy field to the configuration property
Find the configuration registration (typically in a *.contribution.ts file) and add a policy object to the property schema.
Required fields:
Determining `minimumVersion`: Always read version from the root package.json and use the major.minor portion. For example, if package.json has "version": "1.112.0", use minimumVersion: '1.112'. Never hardcode an old version like '1.99'.
policy: {
name: 'MyPolicyName', // PascalCase, unique across all policies
category: PolicyCategory.InteractiveSession, // From PolicyCategory enum
minimumVersion: '1.112', // Use major.minor from package.json version
localization: {
description: {
key: 'my.config.key', // NLS key for the description
value: nls.localize('my.config.key', "Human-readable description."),
}
}
}Optional: `value` function for account-based policy:
If this policy should also be controllable via Copilot account policy data (from IPolicyData), add a value function:
policy: {
name: 'MyPolicyName',
category: PolicyCategory.InteractiveSession,
minimumVersion: '1.112', // Use major.minor from package.json version
value: (policyData) => policyData.my_field === false ? false : undefined,
localization: { /* ... */ }
}The value function receives IPolicyData (from src/vs/base/common/defaultAccount.ts) and should:
- Return a concrete value to override the user's setting
- Return
undefinedto not apply any account-level override (falls through to OS policy or user setting)
If you need a new field on IPolicyData, add it to the interface in src/vs/base/common/defaultAccount.ts.
Optional: `enumDescriptions` for enum/string policies:
IMPORTANT: If the configuration property has type: 'string' and an enum array, you must include enumDescriptions in the localization block with the same number of entries as the enum array. Without this, npm run export-policy-data will fail with: enumDescriptions must exist and have the same length as enum for policy "...".
localization: {
description: { key: '...', value: nls.localize('...', "...") },
enumDescriptions: [
{ key: 'opt.none', value: nls.localize('opt.none', "No access.") },
{ key: 'opt.all', value: nls.localize('opt.all', "Full access.") },
]
}Step 2 — Ensure PolicyCategory is imported
import { PolicyCategory } from '../../../../base/common/policy.js';Existing categories in the PolicyCategory enum:
ExtensionsIntegratedTerminalInteractiveSession(used for all chat/Copilot policies)TelemetryUpdate
If you need a new category, add it to PolicyCategory in src/vs/base/common/policy.ts and add corresponding PolicyCategoryData localization.
Step 3 — Validate TypeScript compilation
Check the VS Code - Build watch task output, or run:
npm run typecheck-clientStep 4 — Export the policy data
Regenerate the auto-generated policy catalog:
npm run export-policy-dataThis script handles transpilation, sets up GITHUB_TOKEN (via gh CLI or GitHub OAuth device flow), and runs --export-policy-data. The export command reads extension configuration policies from the distro's product.json via the GitHub API and merges them into the output.
This updates build/lib/policies/policyData.jsonc. Never edit this file manually. Verify your new policy appears in the output. You will need code review from a codeowner to merge the change to main.
Policy for extension-provided settings
Extension authors cannot add policy: fields directly—their settings are defined in the extension's package.json, not in VS Code core. Instead, policies for extension settings are defined in vscode-distro's product.json under the extensionConfigurationPolicy key.
How it works
1. Source of truth: The extensionConfigurationPolicy map lives in vscode-distro under mixin/{quality}/product.json (stable, insider, exploration). 2. Runtime: When VS Code starts with a distro-mixed product.json, configurationExtensionPoint.ts reads extensionConfigurationPolicy and attaches matching policy objects to extension-contributed configuration properties. 3. Export/build: The --export-policy-data command fetches the distro's product.json at the commit pinned in package.json and merges extension policies into the output. Use npm run export-policy-data which sets up authentication automatically.
Distro format
Each entry in extensionConfigurationPolicy must include:
"extensionConfigurationPolicy": {
"publisher.extension.settingName": {
"name": "PolicyName",
"category": "InteractiveSession",
"minimumVersion": "1.99",
"description": "Human-readable description."
}
}name: PascalCase policy name, unique across all policiescategory: Must be a validPolicyCategoryenum value (e.g.,InteractiveSession,Extensions)minimumVersion: The VS Code version that first shipped this policydescription: Human-readable description string used to generate localization key/value pairs for ADMX/ADML/macOS/Linux policy artifacts
Adding a new extension policy
1. Add the entry to extensionConfigurationPolicy in all three quality product.json files in vscode-distro (mixin/stable/, mixin/insider/, mixin/exploration/) 2. Update the distro commit hash in package.json to point to the distro commit that includes your new entry — the export command fetches extension policies from the pinned distro commit 3. Regenerate policyData.jsonc by running npm run export-policy-data (see Step 4 above) 4. Update the test fixture at src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json with the new entry
Test fixtures
The file src/vs/workbench/contrib/policyExport/test/node/extensionPolicyFixture.json is a test fixture that must stay in sync with the extension policies in the checked-in policyData.jsonc. When extension policies are added or changed in the distro, this fixture must be updated to match — otherwise the integration test will fail because the test output (generated from the fixture) won't match the checked-in file (generated from the real distro).
Downstream consumers
| Consumer | What it reads | Output |
|---|---|---|
policyGenerator.ts | policyData.jsonc | ADMX/ADML (Windows GP), .mobileconfig (macOS), policy.json (Linux) |
vscode-website (gulpfile.policies.js) | policyData.jsonc | Enterprise policy reference table at code.visualstudio.com/docs/enterprise/policies |
vscode-docs | Generated from website build | docs/enterprise/policies.md |
GitHub Preview Features
If your setting is a GitHub Preview Feature — meaning it's a Copilot/chat feature that organizations can disable via their GitHub account-level policy — you must add a value function that checks policyData.chat_preview_features_enabled.
When to add this flag
Add the chat_preview_features_enabled check when all of these apply:
- The setting controls a Copilot or chat feature (e.g., agent tools, hooks, MCP, auto-approve)
- The feature is in preview or experimental status (typically tagged
'preview'or'experimental') - An organization admin should be able to disable it for all users in their org via GitHub account policy
How it works
The chat_preview_features_enabled field on IPolicyData (defined in src/vs/base/common/defaultAccount.ts) is populated from the user's GitHub Copilot token entitlements. When an organization admin disables preview features, chat_preview_features_enabled is set to false.
Pattern
Add a value function to the policy that returns a disabling value when chat_preview_features_enabled === false, and undefined otherwise (to fall through to the user's own setting):
policy: {
name: 'MyPreviewFeaturePolicy',
category: PolicyCategory.InteractiveSession,
minimumVersion: '1.xx', // Must match the first VS Code release that ships this policy.
value: (policyData) => policyData.chat_preview_features_enabled === false ? false : undefined,
localization: {
description: {
key: 'my.setting.description',
value: nls.localize('my.setting.description', "Description of the setting."),
}
}
}Key details:
- Always compare with `=== false`, not
!policyData.chat_preview_features_enabled— the field is optional andundefinedmeans "no policy data available", which should not disable the feature. - Return `undefined` when the flag is not
falseso the account-level policy does not override the user's setting. - Return the disabling value for the setting's type:
falsefor booleans, a restrictive string/enum value for other types.
Real-world examples
See chat.tools.global.autoApprove and chat.useHooks in src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts for existing settings that use this pattern.
Enterprise Managed Settings (native MDM / GitHub server)
GitHub Copilot enterprise admins can lock settings through a managed-settings bag. VS Code feeds the bag from two channels: native MDM (Windows registry / macOS plist) and the GitHub /copilot_internal/managed_settings endpoint. (The external managed-settings-schema.json also describes a managed-settings.json file channel, but VS Code does not read such a file.) Both VS Code channels converge on IPolicyData.managedSettings (a flat dot-path bag) and are consumed by the existing policy.value(policyData) callback — there is no new IPolicyService.
To drive a policy from a managed setting, declare managedSettings on the policy and read policyData.managedSettings?.[KEY] in value (the real ChatToolsAutoApprove also ORs in chat_preview_features_enabled === false):
// Existing policy shown verbatim; `minimumVersion: '1.99'` is its historical value —
// a NEW policy derives minimumVersion from package.json major.minor (see Step 1).
policy: {
name: 'ChatToolsAutoApprove',
category: PolicyCategory.InteractiveSession,
minimumVersion: '1.99',
value: (policyData) =>
policyData.managedSettings?.[COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY] === 'disable'
|| policyData.chat_preview_features_enabled === false ? false : undefined,
managedSettings: {
[COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY]: { type: 'string' },
},
localization: { /* ... */ }
}This is its own modality — full details, schema source of truth, helpers, wiring, and the new-key checklist are in [github-managed-settings.md](./github-managed-settings.md). Read it before adding or reviewing any managed-settings key.
Testing locally: to exercise the account/managed-settings flow without the real GitHub backend, use the mock policy server — see [local-testing.md](./local-testing.md).
One Policy for Many Settings (policyReference)
A single policy can govern multiple settings (e.g. gate an agent in both the editor window and the Agents window). The owner declares the full policy: { name, … }; other settings declare policyReference: { name } pointing at the owner's policy name.
// Owner setting (existing policy; `minimumVersion: '1.126'` is its historical value —
// a NEW policy uses package.json major.minor, see Step 1)
policy: { name: 'Codex3PIntegration', category: PolicyCategory.InteractiveSession, minimumVersion: '1.126', /* ... */ }
// Subordinate setting (no type/value/localization of its own)
policyReference: { name: 'Codex3PIntegration' }policyReference is not managed-settings-specific: use it whenever one enterprise policy should lock multiple settings to the same value. The reference is a pure pointer. It contributes no type, value, managedSettings, restrictedValue, or localization of its own; the owner remains the single source of truth for policy metadata and runtime behavior.
Key rules and internals:
- A setting must not declare both
policyandpolicyReference(rejected during
configuration registration).
- Exactly one setting may own a policy name with
policy; additional settings attach
with policyReference.
- The reference setting's type must match the owner's type;
npm run export-policy-data
enforces this because the same resolved policy value is applied verbatim to owner and references.
ConfigurationRegistry.getPolicyReferenceConfigurations()tracks `policyName →
Set<settingKey>, and PolicyConfiguration` updates both the owner setting and all registered references when the policy value changes.
AbstractPolicyService.serialize()usestoSerializablePolicyDefinition()to strip
the non-cloneable value() callback before sending policy definitions over IPC.
AbstractPolicyService.updatePolicyDefinitions()replaces definitions per policy
name, so a late-registering owner supersedes an earlier reference fallback; if the owner is removed, a reference can still provide a bare type fallback.
- Exported policy data includes
referencedSettingsfor references that are registered
during export, and Developer: Policy Diagnostics lists registered owner/reference settings under the same policy name.
For managed-settings-specific examples that combine policyReference with Copilot managed settings, see github-managed-settings.md.
Examples
Search the codebase for policy: to find all the examples of different policy configurations.
Learnings
- Never hand-edit
build/lib/policies/policyData.jsonc(its header explicitly forbids it). Ifnpm run export-policy-datais failing, fix the script — don't patch the JSON. Common cause: running it in the wrong working directory (e.g. main repo instead of a worktree), which silently exports the wrong source tree. - Document behavior and business-logic expectations, not copy-pasted implementation. Reproducing internal code (e.g. the
getPolicyData()merge body) in the skill rots the moment the source changes and adds no information beyond the source itself. State the contract in prose (e.g. "server-delivered managed settings win over native MDM; the two layers are never merged") and point to the source for the implementation. Reserve code blocks for the author-facing API contract a contributor must follow — how to declare apolicy/managedSettings/valuecallback — not for restating runtime plumbing.
GitHub Copilot Managed Settings
This file documents the managed-settings modality: how an enterprise admin's Copilot configuration (delivered to VS Code via native MDM or the GitHub server) flows into VS Code's policy stack and locks a setting. It is a companion to SKILL.md — read that first for the general policy lifecycle (policy: field, export, artifacts).
Managed settings layer on top of the existing policy framework. They do not introduce a new IPolicyService; they feed IPolicyData.managedSettings, which the existing policy.value(policyData) callback already consumes via AccountPolicyService.
The big idea: one canonical bag, two delivery channels (in VS Code)
Every enterprise-managed Copilot setting resolves through a single normalized bag:
// src/vs/base/common/policy.ts
export type PolicyValue = string | number | boolean;
export type ManagedSettingValue = PolicyValue;
export type ManagedSettingsData = Readonly<Record<string, ManagedSettingValue>>;…surfaced on IPolicyData.managedSettings (src/vs/base/common/defaultAccount.ts). The real JSDoc there summarizes it as: a normalized bag keyed by dot-separated paths (e.g. permissions.disableBypassPermissionsMode), the single channel for enterprise-managed config so server-delivered and native MDM settings resolve identically, with structured settings (e.g. enabledPlugins, extraKnownMarketplaces) carried as canonical JSON strings:
export interface IPolicyData {
// ...
readonly managedSettings?: ManagedSettingsData;
}Keys are flat dot-paths. Scalar leaves flatten directly. Structured values (objects/arrays such as enabledPlugins / extraKnownMarketplaces) are carried as a JSON string under a single key — the same shape an admin authors via native MDM — and parsed back into the object-typed setting on read by PolicyConfiguration.
Delivery channels
VS Code implements two channels feeding the bag; the external schema additionally describes a file-based channel that other Copilot clients may implement but that VS Code does not read today (no managed-settings.json reader exists in src/).
| Channel | Where it's read | Implementation | Lands on |
|---|---|---|---|
| Native MDM (Windows registry / macOS plist) | OS managed preferences | CopilotManagedSettingsService (src/vs/platform/policy/node/copilotManagedSettingsService.ts) via @vscode/policy-watcher | ICopilotManagedSettingsService.managedSettings |
Server-managed (/copilot_internal/managed_settings) | GitHub endpoint; per the code comment in managedSettings.ts, it returns the enterprise's .github/copilot/settings.json content | adaptManagedSettings (src/vs/workbench/services/accounts/browser/managedSettings.ts) → DefaultAccountService.policyData | accountPolicyData.managedSettings |
File-based (managed-settings.json) | external schema only | not implemented in VS Code | — |
Both VS Code channels converge in AccountPolicyService.getPolicyData().
Precedence: server-delivered managed settings win over native MDM. There is a single authoritative source at any point in time — the two layers are not merged. When the server delivers managed settings, native MDM (nativeManagedSettings) is ignored entirely; native MDM applies only when the server provides no managed settings. Rationale: the server is harder to bypass than local MDM/file policies, and admins need one authoritative source to reason about. The winning layer is then projected onto the declared schema (see below). Client-side merging still happens within the winning layer (e.g. enabledPlugins, extraKnownMarketplaces).
Schema source of truth
When the developer has copilot-agent-runtime checked out side-by-side, reference copilot-agent-runtime/schema/managed-settings-schema.json as the authoritative shape. It is aligned with the managed_settings API output and is the schema for all delivery channels (MDM plist/registry, file-based, server-managed). Its top-level properties today are permissions, enabledPlugins, extraKnownMarketplaces, and strictKnownMarketplaces (nested objects/arrays). Note the schema is nested, whereas the VS Code bag is flattened to dot-paths — e.g. the schema's nested permissions.disableBypassPermissionsMode becomes the flat bag key of the same name (the COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY constant):
| Schema property (path) | Type in schema | Composition (x-composition.strategy) |
|---|---|---|
permissions.disableBypassPermissionsMode | string enum "disable" | most-restrictive-wins (sticky once set) |
enabledPlugins | { "PLUGIN@MARKETPLACE": boolean } | deny-wins (false beats true; enterprise denials immutable) |
extraKnownMarketplaces | { name: { source } }, source github \ | git \ |
strictKnownMarketplaces | array of source descriptors | most-restrictive-wins (empty array = lockdown) |
Current schema ↔ runtime divergences (treat managed-settings-schema.json as theAPI source of truth, and keep the VS Code managedSettings type declarations alignedwith what the server actually projects into the bag):
- strictKnownMarketplaces: schema models an array allowlist, but VS Code declaresCOPILOT_STRICT_MARKETPLACES_KEYas a boolean flag ({ type: 'boolean' }on
ChatStrictMarketplaces).-extraKnownMarketplaces: the schema permits source kindsgithub/git/
directory, but the VS Code normalizer only acceptsgithubandgit—
directory (and any other kind) is dropped with a warning(managedSettings.tsnormalizeExtraKnownMarketplaces;IExtraKnownMarketplaceEntry
inbase/common/managedSettings.tsonly typesgithub/git).
Note the schema's x-composition describes the server/runtime layering across enterprise/org/user. Inside VS Code the bag has already been collapsed to a single projected ManagedSettingsData before a policy.value() callback ever sees it.
Declaring a managed setting on a policy
A policy that should be driven by a managed-settings key declares two things on its IPolicy object (src/vs/base/common/policy.ts):
1. managedSettings — the dot-path keys it reads, with their value type. 2. A value(policyData) callback that reads policyData.managedSettings?.[KEY].
Use the exported key constants from src/vs/platform/policy/common/copilotManagedSettings.ts — never inline the strings:
import {
COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY,
} from '../../../../platform/policy/common/copilotManagedSettings.js';
// chat.tools.global.autoApprove — owns ChatToolsAutoApprove (existing policy, shown verbatim)
policy: {
name: 'ChatToolsAutoApprove',
category: PolicyCategory.InteractiveSession,
minimumVersion: '1.99', // existing value — for a NEW policy use package.json major.minor (see SKILL.md Step 1)
value: (policyData) =>
policyData.managedSettings?.[COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY] === 'disable'
|| policyData.chat_preview_features_enabled === false
? false
: undefined,
managedSettings: {
[COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY]: { type: 'string' },
},
localization: { description: { /* ... */ } },
}Key rules for the value callback:
- Read from
policyData.managedSettings?.[KEY]— never a typed field onIPolicyData
(the typed enabledPlugins / extraKnownMarketplaces / strictKnownMarketplaces fields were removed; everything is the canonical bag now).
- Return the locking value when the managed setting demands it,
undefinedotherwise
(so the user's setting falls through).
- It's fine to combine with
chat_preview_features_enabled === false(see SKILL.md's
"GitHub Preview Features" section).
Structured (object/array) settings
For settings whose type is 'object' or 'array', the policy still declares the managed-settings key as a string (the JSON is carried as a string), and the value callback returns that raw string. When the policy value is a string but the setting's type is not, PolicyConfiguration parses it back into the typed value on read via its own lenient JSONC parser (PolicyConfiguration.parse() using a json.visit streaming visitor — not JSON.parse; see configurations.ts). Examples in chat.shared.contribution.ts:
// chat.plugins.enabledPlugins — type: 'object'
value: (policyData) => policyData.managedSettings?.[COPILOT_ENABLED_PLUGINS_KEY],
managedSettings: { [COPILOT_ENABLED_PLUGINS_KEY]: { type: 'string' } },ChatExtraMarketplaces (chat.plugins.extraMarketplaces) is policy-only (included: false) — there is no user-writable surface for it; it exists solely as a delivery slot for the managed value.
How the pieces fit (helpers in copilotManagedSettings.ts)
| Function | Role |
|---|---|
flattenManagedSettings(obj) | Flattens a nested response into dot-path scalars (used by the server adapter). |
collectManagedSettingsDefinitions(policyDefinitions) | Aggregates every policy's managedSettings into one key → { type } map. Single source of truth for which keys (and types) are honored; drives both the MDM watcher and the server projection. |
projectManagedSettings(values, definitions, onWarn?) | Keeps only declared keys whose runtime value matches the declared type. Undeclared keys and type mismatches are dropped (validated, never coerced), with an optional warning. |
Constants (also in copilotManagedSettings.ts):
| Constant | Value |
|---|---|
GITHUB_COPILOT_WIN32_REGISTRY_PATH | SOFTWARE\Policies\GitHubCopilot |
GITHUB_COPILOT_WIN32_POLICY_NAME | GitHubCopilot (productName for the watcher) |
GITHUB_COPILOT_MACOS_BUNDLE_ID | com.github.copilot (CFPreferences app id) |
COPILOT_DISABLE_BYPASS_PERMISSIONS_MODE_KEY | permissions.disableBypassPermissionsMode |
COPILOT_ENABLED_PLUGINS_KEY | enabledPlugins |
COPILOT_EXTRA_MARKETPLACES_KEY | extraKnownMarketplaces |
COPILOT_STRICT_MARKETPLACES_KEY | strictKnownMarketplaces |
Wiring (where the MDM service is constructed)
Native MDM is desktop-main only (src/vs/code/electron-main/main.ts). The real wiring constructs the platform service first (Windows / macOS only), then registers it — falling back to NullCopilotManagedSettingsService on Linux (abbreviated):
let copilotManagedSettingsService: CopilotManagedSettingsService | undefined;
if (isWindows) {
copilotManagedSettingsService = new CopilotManagedSettingsService(
logService, GITHUB_COPILOT_WIN32_POLICY_NAME, { registryPath: GITHUB_COPILOT_WIN32_REGISTRY_PATH });
} else if (isMacintosh) {
copilotManagedSettingsService = new CopilotManagedSettingsService(
logService, GITHUB_COPILOT_MACOS_BUNDLE_ID);
}
if (copilotManagedSettingsService) {
services.set(ICopilotManagedSettingsService, copilotManagedSettingsService);
} else {
services.set(ICopilotManagedSettingsService, new NullCopilotManagedSettingsService());
}It is exposed to the renderer over IPC via CopilotManagedSettingsChannel / CopilotManagedSettingsChannelClient (copilotManagedSettingsIpc.ts), registered as the copilotManagedSettings channel in app.ts. AccountPolicyService subscribes to onDidChangeManagedSettings and re-evaluates policy values when managed settings change.
The service only watches keys that some policy declares: updatePolicyDefinitions calls collectManagedSettingsDefinitions, then @vscode/policy-watcher watches exactly those dot-paths. No declared keys ⇒ no watcher.
Adding a brand-new managed-settings key (checklist)
1. Pick the canonical dot-path and add it as a constant in copilotManagedSettings.ts. It must match the server managed_settings API field / the managed-settings-schema.json key exactly. 2. Attach it to a policy on the governing setting: add managedSettings: { [KEY]: { type } } and a value(policyData) that reads policyData.managedSettings?.[KEY]. 3. If the value is structured (object/array), declare the managed key as 'string', return the raw JSON string from value, and let PolicyConfiguration parse it. If the server response shape differs from the stored shape, normalize it in adaptManagedSettings (server side) so the bag matches what an admin would author in MDM. 4. Keep the schema aligned. The runtime, the server endpoint, and managed-settings-schema.json must agree on the key name and value type. The declaration-driven projection (projectManagedSettings) silently drops anything that doesn't match the declared type, so a type drift = a silently ignored setting. 5. Export & test as in SKILL.md Step 3–4 (npm run typecheck-client, npm run export-policy-data). Verify the policy appears in policyData.jsonc.
Reference tests:
src/vs/platform/policy/test/common/copilotManagedSettings.test.tssrc/vs/platform/policy/test/node/copilotManagedSettingsService.test.tssrc/vs/workbench/services/policies/test/browser/accountPolicyService.test.tssrc/vs/workbench/services/accounts/test/browser/managedSettings.test.ts
(includes an end-to-end equivalence test: a server JSON string and a native MDM JSON string resolve to the identical typed object).
Manual/local testing: use the mock policy server to serve arbitrary managed_settings (and entitlement/token) responses and apply them via Developer: Sync Account Policy — see local-testing.md.
Related: one policy governing many settings (policyReference)
A single enterprise policy can lock more than one setting — e.g. gate an agent in both the editor window and the Agents window. This is the policyReference mechanism (src/vs/base/common/policy.ts → IPolicyReference).
- The owner setting declares the full
policy: { name, … }(type, metadata, runtime
value/managedSettings). Exactly one setting may own a given policy name.
- Other settings declare
policyReference: { name }pointing at that owner's policy name.
A reference is a pure pointer: no type, no value, no localization. It only contributes the name so the setting is gated and the OS watcher observes the name in processes where the owner module isn't loaded.
// Owner: chat.agentHost.codexAgent.enabled (existing policy, shown verbatim;
// `minimumVersion: '1.126'` is its historical value — a NEW policy uses package.json major.minor)
policy: { name: 'Codex3PIntegration', category: PolicyCategory.InteractiveSession,
minimumVersion: '1.126', value: (d) => d.chat_preview_features_enabled === false ? false : undefined,
localization: { /* ... */ } }
// Reference: sessions.chat.claudeAgent.enabled and chat.agentHost.claudeAgent.enabled
policyReference: { name: 'Claude3PIntegration' } // owned by github.copilot.chat.claudeAgent.enabledRules & internals:
- Cannot declare both
policyandpolicyReferenceon the same setting (rejected at
registration in configurationRegistry.ts).
- The reference's
typemust match the owner's (enforced when exporting the catalog). ConfigurationRegistry.getPolicyReferenceConfigurations()returnsname → Set<settingKey>.
PolicyConfiguration applies the owner's resolved policy value to every reference key too.
- IPC-safe serialization:
toSerializablePolicyDefinition()strips the non-cloneable
value() callback so a policy registered in the main process survives structured-clone to the renderer. AbstractPolicyService.updatePolicyDefinitions replaces per name, so a late-registering owner supersedes an earlier-registered reference (and removing the owner falls back to the reference's bare type).
- Catalog & diagnostics: the exported
PolicyDtogainsreferencedSettings: string[]
(sorted; omitted when a policy governs only its owner). The exporter only captures references that are registered/loaded at export time, so the checked-in policyData.jsonc can list fewer references than the source declares — e.g. Claude3PIntegration lists only chat.agentHost.claudeAgent.enabled, not the sessions.chat.claudeAgent.enabled reference declared in the sessions contribution. At runtime, the Developer: Policy Diagnostics report (developerActions.ts) lists every registered setting each policy governs, owner + references.
What changed (PR history)
| PR | Change |
|---|---|
| #318623 | Wire /copilot_internal/managed_settings into AccountPolicyService/IPolicyData; add chat.plugins.enabledPlugins/extraMarketplaces/strictMarketplaces settings + adaptManagedSettings shape adaptation. No new IPolicyService. |
| #320991 | Add native MDM delivery: CopilotManagedSettingsService + @vscode/policy-watcher; let policies declare managedSettings mappings; wire the first V0 key permissions.disableBypassPermissionsMode → force ChatToolsAutoApprove=false. |
| #321218 | Make IPolicyData.managedSettings the single channel: server + native MDM project into one canonical bag; structured settings carried as canonical JSON strings; remove the typed enabledPlugins/extraKnownMarketplaces/strictKnownMarketplaces fields. End-to-end equivalence test. |
| #321515 | Add policyReference so one policy governs many settings; callback-free serialization; catalog + diagnostics list governed settings. Used to gate Claude (Claude3PIntegration) and Codex (Codex3PIntegration) across the editor and Agents windows. |
Local Testing: Mock Policy Server
Use the mock policy server to serve arbitrary Copilot policy responses locally. It mocks the four defaultChatAgent endpoints that DefaultAccountService calls.
Quick start
npm run mock-policy-server # http://127.0.0.1:3000Open the GUI, edit the JSON response for any endpoint, Save, then click Wire all endpoints (writes product.overrides.json). Reload Code OSS, sign in, and run Developer: Sync Account Policy to pull the mocked data.
Click Unwire when done — it restores the original product.overrides.json from a backup. Use Copy overrides JSON if you prefer to paste manually.
Schema validation (Managed Settings tab)
Expand the Schema section, point the path at a local managed-settings-schema.json, and click Load. The path is saved in localStorage across reloads. Click Validate to check for unknown keys.
Related skills
FAQ
What does add-policy do?
Use when adding, modifying, or reviewing VS Code configuration policies. Covers the full policy lifecycle from registration to export to platform-specific artifacts. Run on ANY change tha...
When should I use add-policy?
Invoke when Use when adding, modifying, or reviewing VS Code configuration policies. Covers the full policy lifecycle from registration to export to pla.
Is add-policy safe to install?
Review the Security Audits panel on this page before installing in production.