Remote Managed Settings

Description

Enterprise managed settings synchronization service at src/services/remoteManagedSettings/. Fetches organization-managed configuration from the Anthropic API (/api/claude_code/settings), caches it locally as ~/.claude/remote-settings.json, and feeds it into the five-layer-architecture's settings merge pipeline as the policySettings layer -- the highest-priority settings source that cannot be overridden by user, project, or local settings.

The service enables centralized policy enforcement for Teams and Enterprise organizations: administrators define settings once on the server, and every Claude Code client in the organization picks them up automatically. The system is designed to fail open -- if the fetch fails and no cache exists, the client continues without remote settings rather than blocking.

Sync mechanism

Initialization and loading

  1. initializeRemoteManagedSettingsLoadingPromise() is called early during startup (in init.ts). It creates a promise that other subsystems can await via waitForRemoteManagedSettingsToLoad(), ensuring they do not initialize with stale or missing policy data. A 30-second timeout prevents deadlocks if the load function is never called (e.g., Agent SDK tests).
  2. loadRemoteManagedSettings() runs during CLI initialization. It uses a cache-first strategy: if remote-settings.json exists on disk, it applies those settings and unblocks waiters immediately (saving ~77ms of fetch latency on print-mode startup). The fetch still runs asynchronously; if new settings arrive, a hot-reload notification fires.
  3. After loading, background polling starts to detect mid-session changes.

Fetch protocol

Retry and resilience

Background polling

Polls every 1 hour (POLLING_INTERVAL_MS = 60 * 60 * 1000). On each poll: 1. Snapshots the current cached settings. 2. Fetches fresh settings via the same fetch-and-load pipeline. 3. Compares old vs. new via JSON serialization; if different, fires settingsChangeDetector.notifyChange('policySettings') to trigger hot-reload across the application (env vars, telemetry, permissions).

The polling timer is unref()'d so it does not keep the Node.js process alive, and a cleanup handler stops it on shutdown.

Auth state changes

refreshRemoteManagedSettings() is called on login/logout. It clears all caches (session, file, polling), re-fetches from the API, and fires a change notification.

Eligibility

Not all users query the remote settings endpoint. Eligibility is determined by isRemoteManagedSettingsEligible() in syncCache.ts:

User type Eligible?
Console users with API key Yes
OAuth Enterprise / Team subscribers Yes
OAuth with null subscriptionType (externally-injected tokens, CCD, CCR, Agent SDK, CI) Yes -- API returns empty for ineligible orgs, cost is one round-trip
Third-party provider users (non-firstParty) No
Custom base URL users (non-Anthropic endpoint) No
Cowork / local-agent entrypoint No -- Cowork has its own VM permission model

The eligibility result is cached for the session lifetime to avoid repeated auth-chain evaluation.

Manageable settings

Remote settings use the same SettingsSchema as all other settings sources. Key categories an administrator can configure remotely:

Category Example fields
Permissions permissions.allow, permissions.deny, permissions.defaultMode, permissions.disableBypassPermissionsMode
Model controls model, availableModels (allowlist), modelOverrides (provider-specific IDs)
Environment variables env (record of key-value pairs)
MCP server policy allowedMcpServers, deniedMcpServers, allowManagedMcpServersOnly
Hooks hooks, allowManagedHooksOnly, allowedHttpHookUrls
Plugin policy strictPluginOnlyCustomization, strictKnownMarketplaces, blockedMarketplaces
Auth helpers apiKeyHelper, awsCredentialExport, awsAuthRefresh, gcpAuthRefresh
Login forceLoginMethod, forceLoginOrgUUID
Telemetry otelHeadersHelper and OTEL env vars
Sandbox sandbox settings
UX companyAnnouncements, outputStyle, language, spinnerVerbs
SSH configs sshConfigs for pre-configured remote environments
Permission rules allowManagedPermissionRulesOnly to lock down non-admin permission rules

Settings merge and conflict resolution

Settings are merged across five layers in ascending priority order:

pluginSettings -> userSettings -> projectSettings -> localSettings -> flagSettings -> policySettings

policySettings is the highest-priority layer. Because SETTING_SOURCES defines it last, it wins over all other sources when keys conflict. The merge uses lodash mergeWith with a custom settingsMergeCustomizer that concatenates arrays (rather than replacing them).

Within policySettings, a first-source-wins strategy selects a single origin:

  1. Remote (API-fetched, highest) -- remote-settings.json
  2. MDM admin (HKLM registry on Windows / macOS plist) -- admin-only
  3. File-based (managed-settings.json + managed-settings.d/*.json drop-ins) -- requires admin filesystem access
  4. HKCU (Windows user-writable registry) -- lowest

Only the first non-empty source is used; they do not merge with each other. This means remote settings completely replace file-based managed settings when present.

policySettings and flagSettings are always loaded regardless of --setting-sources CLI flag. They cannot be disabled by the user.

Security check for dangerous settings

When new remote settings arrive with content, a security check runs before applying them (securityCheck.tsx). Settings are classified as "dangerous" if they contain:

  1. Shell-executing settings: apiKeyHelper, awsAuthRefresh, awsCredentialExport, gcpAuthRefresh, otelHeadersHelper, statusLine -- these can execute arbitrary shell code.
  2. Unsafe environment variables: Any env variable NOT in the SAFE_ENV_VARS allowlist. Dangerous env vars include those that redirect to attacker-controlled servers (ANTHROPIC_BASE_URL, proxy vars, OTEL endpoints), trust attacker-controlled servers (NODE_TLS_REJECT_UNAUTHORIZED, NODE_EXTRA_CA_CERTS), or switch to attacker-controlled projects (ANTHROPIC_API_KEY, AWS_BEARER_TOKEN_BEDROCK).
  3. Hooks: Any non-empty hooks configuration.

If dangerous settings are detected and have changed from the cached version, an interactive blocking dialog (ManagedSettingsSecurityDialog) is shown. The user can: - Accept -- settings are applied and cached. - Reject -- the process exits via gracefulShutdownSync(1). Cached settings are preserved for next session.

The dialog is skipped in non-interactive mode (consistent with the trust dialog). Analytics events are logged for dialog shown/accepted/rejected (tengu_managed_settings_security_dialog_*).

File layout

File Responsibility
index.ts Public API, fetch logic, retry, background polling, save/clear
types.ts Zod schema for API response (RemoteManagedSettingsResponseSchema), fetch result type
syncCache.ts Eligibility check (isRemoteManagedSettingsEligible), auth-touching logic
syncCacheState.ts Session cache state, disk I/O for remote-settings.json, eligibility tri-state
securityCheck.tsx Security dialog for dangerous settings approval

Key claims

Relations

Sources