Skip to content

feat: add macOS managed preferences support for enterprise MDM deployments#19178

Merged
rekram1-node merged 8 commits intoanomalyco:devfrom
lennyvaknine43:feat/macos-managed-preferences
Apr 2, 2026
Merged

feat: add macOS managed preferences support for enterprise MDM deployments#19178
rekram1-node merged 8 commits intoanomalyco:devfrom
lennyvaknine43:feat/macos-managed-preferences

Conversation

@lennyvaknine43
Copy link
Copy Markdown
Contributor

@lennyvaknine43 lennyvaknine43 commented Mar 25, 2026

Issue for this PR

Closes #19158

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds support for reading macOS managed preferences deployed via .mobileconfig / MDM (Jamf, Kandji, FleetDM). On macOS, readManagedPreferences() checks for a plist at /Library/Managed Preferences/ under the ai.opencode.managed preference domain, converts it to JSON via plutil, strips MDM metadata keys (PayloadUUID, _manualProfile, etc.), and merges the result at the highest priority tier — above file-based managed config, project config, and user config.

This is the standard macOS mechanism for enterprise config enforcement (same pattern as Claude Code's com.anthropic.claudecode domain). The plist is root-owned and only writable by MDM, so users cannot override it. Every existing opencode.json key works as a plist key — no schema changes needed.

The changes are ~35 lines in config.ts, 2 lines in preload.ts for test isolation, and 5 tests covering override behavior and graceful missing-plist handling.

Available settings reference

Every key from opencode.json works as a managed preference key. The plist structure maps 1:1 to the JSON config:

Key Type Values / Description
share string "manual" | "auto" | "disabled"
autoupdate boolean | string true | false | "notify"
model string "provider/model" (e.g. "anthropic/claude-sonnet-4-5")
small_model string "provider/model" — model for lightweight tasks
default_agent string Name of default agent ("build", "plan", or custom)
username string Display name in conversations
enabled_providers string[] When set, ONLY these providers are loaded
disabled_providers string[] Providers to exclude from auto-loading
snapshot boolean Enable/disable filesystem snapshot tracking
server.hostname string Bind address (e.g. "127.0.0.1")
server.port integer Port number
server.mdns boolean Enable/disable mDNS service discovery
server.cors string[] Additional CORS domains
permission.* string Global default: "ask" | "allow" | "deny"
permission.read string | object "ask" | "allow" | "deny" or { "pattern": "action" }
permission.edit string | object Same as read
permission.bash string | object Same — object form allows per-command rules
permission.glob string "ask" | "allow" | "deny"
permission.grep string "ask" | "allow" | "deny"
permission.list string "ask" | "allow" | "deny"
permission.task string | object "ask" | "allow" | "deny"
permission.skill string | object "ask" | "allow" | "deny"
permission.webfetch string "ask" | "allow" | "deny"
permission.websearch string "ask" | "allow" | "deny"
permission.todowrite string "ask" | "allow" | "deny"
permission.external_directory string | object "ask" | "allow" | "deny"
permission.doom_loop string "ask" | "allow" | "deny"
permission.lsp string | object "ask" | "allow" | "deny"
permission.<path-glob> string Top-level path deny (e.g. "~/.ssh/*": "deny")
compaction.auto boolean Enable auto-compaction when context is full
compaction.prune boolean Enable pruning of old tool outputs
mcp.<name>.type string "local" | "remote"
mcp.<name>.url string Remote MCP server URL
mcp.<name>.command string[] Local MCP server command
mcp.<name>.enabled boolean Enable/disable the MCP server
instructions string[] Additional instruction file paths
plugin string[] Plugin specifiers
Usage guide for MDM admins

1. Create a .mobileconfig

The plist keys map directly to opencode.json fields:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>PayloadContent</key>
  <array>
    <dict>
      <key>PayloadType</key>
      <string>ai.opencode.managed</string>
      <key>PayloadIdentifier</key>
      <string>com.example.opencode.config</string>
      <key>PayloadUUID</key>
      <string>GENERATE-YOUR-OWN-UUID</string>
      <key>PayloadVersion</key>
      <integer>1</integer>
      <key>share</key>
      <string>disabled</string>
      <key>server</key>
      <dict>
        <key>hostname</key>
        <string>127.0.0.1</string>
      </dict>
      <key>permission</key>
      <dict>
        <key>*</key>
        <string>ask</string>
        <key>bash</key>
        <dict>
          <key>*</key>
          <string>ask</string>
          <key>rm -rf *</key>
          <string>deny</string>
        </dict>
      </dict>
    </dict>
  </array>
  <key>PayloadType</key>
  <string>Configuration</string>
  <key>PayloadIdentifier</key>
  <string>com.example.opencode</string>
  <key>PayloadUUID</key>
  <string>GENERATE-YOUR-OWN-UUID</string>
  <key>PayloadVersion</key>
  <integer>1</integer>
</dict>
</plist>

2. Deploy via MDM

Jamf Pro: Computers > Configuration Profiles > Upload > scope to target devices

FleetDM: Add the .mobileconfig under mdm.macos_settings.custom_settings and run fleetctl apply

3. Verify on a device

Double-click the .mobileconfig to install locally for testing (shows in System Settings > Privacy & Security > Profiles), then:

plutil -p "/Library/Managed Preferences/$(whoami)/ai.opencode.managed.plist"
opencode debug config

How did you verify your code works?

  • Deployed a binary plist to /Library/Managed Preferences/ on a Jamf-enrolled macOS device
  • Verified bun dev debug config resolved all managed settings correctly
  • Set conflicting values in ~/.config/opencode/opencode.json (e.g. share: auto, bash: allow) and confirmed MDM settings override them
  • All 71 config tests pass (66 existing + 5 new), 0 type errors

Screenshots / recordings

N/A — no UI changes.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Mar 25, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@lennyvaknine43 lennyvaknine43 force-pushed the feat/macos-managed-preferences branch from c87ac50 to 1668da0 Compare March 25, 2026 23:34
*/
async function readManagedPreferences(): Promise<Info> {
// Skip OS-level plist reading when test isolation is active
if (process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR) return {}
Copy link
Copy Markdown

@erran erran Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this branch should be removed to avoid users setting this at runtime. Instead existsSync and Process.run could be mocked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep you're right, removed it. the parsing/stripping logic is in a separate exported parseManagedPlist() function that the tests call directly with JSON strings — no env var branches, no mocking

@lennyvaknine43 lennyvaknine43 force-pushed the feat/macos-managed-preferences branch 2 times, most recently from fc82ee9 to 9ca67d3 Compare March 26, 2026 20:09
…ments

Adds parseManagedPlist() and readManagedPreferences() to the config
loader. On macOS, reads from /Library/Managed Preferences/ under
the ai.opencode.managed preference domain (deployed via .mobileconfig
/ Jamf / Kandji / FleetDM). Settings merge at the highest priority
tier, above all other config sources.

parseManagedPlist() is a pure function that strips MDM metadata keys
and parses through the config schema — fully unit-testable with no
OS interaction. readManagedPreferences() handles the OS-level plist
reading via plutil. No env var overrides are exposed.

Includes docs in config.mdx covering file-based and MDM managed
settings, mobileconfig creation, and MDM deployment instructions.

Closes anomalyco#19158
@lennyvaknine43 lennyvaknine43 force-pushed the feat/macos-managed-preferences branch from 9ca67d3 to 571bd0b Compare March 26, 2026 20:12
Lenny Vaknine and others added 7 commits March 30, 2026 12:08
@rekram1-node
Copy link
Copy Markdown
Collaborator

Dax said we can ship this for now but I think we are going to have a better approach that should work better across the board later on

@rekram1-node rekram1-node merged commit 7e32f80 into anomalyco:dev Apr 2, 2026
8 checks passed
hugojosefson pushed a commit to hugojosefson/opencode that referenced this pull request Apr 6, 2026
…ments (anomalyco#19178)

Co-authored-by: Lenny Vaknine <lvaknine@gitlab.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
NicholasDominici added a commit to jairad26/opencode that referenced this pull request Apr 6, 2026
* refactor(init): tighten AGENTS guidance (anomalyco#20422)

* refactor(shell): use Effect ChildProcess for shell command execution (anomalyco#20494)

* refactor: use Effect services instead of async facades in provider, auth, and file (anomalyco#20480)

* chore: generate

* chore: update nix node_modules hashes

* Update VOUCHED list

anomalyco#20482 (comment)

* test(app): migrate more e2e suites to isolated backend (anomalyco#20505)

* fix(account): coalesce concurrent console token refreshes (anomalyco#20503)

* test(app): fix isolated backend follow-ups (anomalyco#20513)

* wip: zen

* refactor: replace BunProc with Npm module using @npmcli/arborist (anomalyco#18308)

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>

* zen: sync

* fix: normalize filepath in FileTime to prevent Windows path mismatch (anomalyco#20367)

Co-authored-by: JosXa <info@josxa.dev>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>

* resolve subpath only packages for plugins (anomalyco#20555)

* Fix selection expansion by retaining focused input selections during global key events (anomalyco#20205)

* feat: add new provider plugin hook for resolving models and sync models from github models endpoint (falls back to models.dev) (anomalyco#20533)

* fix(tui): apply scroll configuration uniformly across all scrollboxes (anomalyco#14735)

* chore(tui): clean up scroll config follow-up (anomalyco#20561)

* fix(opencode): batch snapshot revert without reordering (anomalyco#20564)

* fix(test): auto-acknowledge tool-result follow-ups in mock LLM server (anomalyco#20528)

* chore: generate

* refactor: add Effect-returning versions of MessageV2 functions (anomalyco#20374)

* refactor(bash): use Effect ChildProcess for bash tool execution (anomalyco#20496)

* chore: generate

* Refactor plugin/config loading, add theme-only plugin package support (anomalyco#20556)

* fix(test): use effect helper in snapshot race test (anomalyco#20567)

* refactor(revert): yield SessionSummary.Service directly (anomalyco#20541)

* fix: show model display name in message footer and transcript (anomalyco#20539)

* flock npm.add (anomalyco#20557)

* fix(account): refresh console tokens before expiry (anomalyco#20558)

* chore: add User-Agent headers for Cloudflare providers (anomalyco#20538)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>

* fix(build): replace require() with dynamic import() in cross-spawn-spawner (anomalyco#20580)

* chore: generate

* tui: add consent dialog when sharing for the first time (anomalyco#20525)

* test(app): block real llm calls in e2e prompts (anomalyco#20579)

* refactor(instruction): migrate to Effect service pattern (anomalyco#20542)

* fix(cli): use simple logo in CLI (anomalyco#20585)

* fix(core): prevent agent loop from stopping after tool calls with OpenAI-compatible providers (anomalyco#14973)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>

* fix(session): compaction agent responds in same language as conversation (anomalyco#20581)

Co-authored-by: Aaron Zhu <aaron@Aarons-MacBook-Air.local>

* refactor(account): share token freshness helper (anomalyco#20591)

* fix(cli): restore colored help logo (anomalyco#20592)

* feat(opencode): Add Venice AI package as dependency (anomalyco#20570)

* chore: generate

* cli: update usage exceeded error

* chore: update nix node_modules hashes

* fix(node): set OPENCODE_CHANNEL during build (anomalyco#20616)

* zen: friendly trial ended message

* refactor: simplify solid reactivity across app and web (anomalyco#20497)

* use solid-primitives/resize-observer across web code (anomalyco#20613)

* cleanup event listeners with solid-primitives/event-listener (anomalyco#20619)

* chore: update nix node_modules hashes

* refactor: split up models.dev and config model definitions to prevent coupling (anomalyco#20605)

* chore: generate

* feat: add optional messageID to ShellInput (anomalyco#20657)

* chore: generate

* ignore: fix typecheck in dev (anomalyco#20702)

* fix(format): use biome format instead of check to prevent import removal (anomalyco#20545)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>

* tweak: add abort signal timeout to the github copilot model fetch to prevent infinite blocking (anomalyco#20705)

* Add MiMo-V2 models to Go UI and docs (anomalyco#20709)

* refactor(format): update formatter interface to return command from enabled() (anomalyco#20703)

* app: unify auto scroll ref handling (anomalyco#20716)

* go: add mimo

* electron: add basic context menu for inspect element (anomalyco#20723)

* feat: add macOS managed preferences support for enterprise MDM deployments (anomalyco#19178)

Co-authored-by: Lenny Vaknine <lvaknine@gitlab.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>

* chore: generate

* feat(acp): Add messageID and emit user_message_chunk on prompt/command (anomalyco#18625)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>

* chore: generate

* test(app): add a golden path for mocked e2e prompts (anomalyco#20593)

* chore: update nix node_modules hashes

* docs(effect): refresh migration status (anomalyco#20665)

* test(opencode): remove temporary e2e url repro (anomalyco#20729)

* refactor(app): unexport internal e2e helpers (anomalyco#20730)

* test(app): emit junit artifacts for playwright (anomalyco#20732)

* refactor(todo): effectify session todo (anomalyco#20595)

* Adds TUI prompt traits, refs, and plugin slots (anomalyco#20741)

* chore: update nix node_modules hashes

* dialog aware prompt cursor (anomalyco#20753)

* fix(opencode): honor model limit.input overrides (anomalyco#16306)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>

* refactor(effect): prune unused facades (anomalyco#20748)

* fix: rm dynamic part from bash tool description again to restore cache hits across projects (anomalyco#20771)

* refactor(share): effectify share next (anomalyco#20596)

* fix: call models.dev once instead of twice on start (anomalyco#20765)

* fix: prevent Tool.define() wrapper accumulation on object-defined tools (anomalyco#16952)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: generate

* add automatic heap snapshots for high-memory cli processes (anomalyco#20788)

* feat: Send x-session-affinity and x-parent-session-id headers (anomalyco#20744)

* fix(sdk): handle Windows opencode spawn and shutdown (anomalyco#20772)

* chore: update nix node_modules hashes

* electron: better menus (anomalyco#20878)

* fix(core): fix restoring earlier messages in a reverted chain (anomalyco#20780)

* fix(session): delay jump-to-bottom button (anomalyco#20853)

* fix(prompt): unmount model controls in shell mode (anomalyco#20886)

* fix(session): disable todo dock auto-scroll (anomalyco#20840)

* feat(ui): redesign modified files section in session turn (anomalyco#20348)

Co-authored-by: David Hill <iamdavidhill@gmail.com>

* fix(app): hide default session timestamps (anomalyco#20892)

* refactor(effect): resolve built tools through the registry (anomalyco#20787)

* fix: restore prompt focus after footer selection (anomalyco#20841)

* feat: restore git-backed review modes (anomalyco#20845)

* chore: generate

* fix(app): show correct submit icon when typing follow up

* chore(app): remove queued follow-ups for now

* refactor(effect): build todowrite tool from Todo service (anomalyco#20789)

Co-authored-by: Juan Pablo Carranza Hurtado <52012198+jpcarranza94@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* test(ci): publish unit reports in actions (anomalyco#20547)

* chore: rm models snapshot (anomalyco#20929)

* notes on v2 (anomalyco#20941)

* chore: generate

* refactor(provider): stop custom loaders using facades (anomalyco#20776)

Co-authored-by: luanweslley77 <213105503+luanweslley77@users.noreply.github.com>

* perf(opencode): batch snapshot diffFull blob reads (anomalyco#20752)

Co-authored-by: Nate Williams <50088025+natewill@users.noreply.github.com>

* fix(ci): create JUnit output dirs before tests (anomalyco#20959)

* release: v1.3.14

* refactor: remove redundant Kimi skill section (anomalyco#20393)

Co-authored-by: dongyuxin <dongyuxin@dev.dongyuxin.msh-dev.svc.cluster.local>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>

* fix(npm): Arborist reify fails on compiled binary — Bun pre-resolves node-gyp path at build time (anomalyco#21040)

* release: v1.3.15

* fix: ensure reasoning tokens arent double counted when calculating usage (anomalyco#21047)

* feat(tui): show console-managed providers (anomalyco#20956)

* refactor(effect): move read tool onto defineEffect (anomalyco#21016)

* chore: generate

* fix(tui): only show org switch affordances when useful (anomalyco#21054)

* chore: generate

* test: add regression test for double counting bug (anomalyco#21053)

* doc: udpate doc

* tweak: add newline between <content> and first line of read tool output to prevent confusion (anomalyco#21070)

* fix(plugin): parse package specifiers with npm-package-arg and sanitize win32 cache paths (anomalyco#21135)

* chore: update nix node_modules hashes

* feat(tui): make the mouse disablable (anomalyco#6824, anomalyco#7926) (anomalyco#13748)

* fix(core): implement proper configOptions for acp (anomalyco#21134)

* fix: pass both 'openai' and 'azure' providerOptions keys for @ai-sdk/azure (anomalyco#20272)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>

* fix(tui): default Ctrl+Z to undo on Windows (anomalyco#21138)

* release: v1.3.16

* zen: remove header check

* zen: normalize ipv6

* fix: show clear error when Cloudflare provider env vars are missing (anomalyco#20399)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>

* fix(tui): revert kitty keyboard events workaround on windows (anomalyco#20180)

* release: v1.3.17

* fix(lsp): MEMORY LEAK: ensure typescript server uses native project config (anomalyco#19953)

* chore: generate

* docs: update Cloudflare provider setup to reflect /connect prompt flow (anomalyco#20589)

* refactor: replace Bun.serve with Node http.createServer in OAuth handlers (anomalyco#18327)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>

* upgrade opentui to 0.1.97 (anomalyco#21137)

* refactor(server): replace Bun serve with Hono node adapters (anomalyco#18335)

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>

* chore: update nix node_modules hashes

* tweak: ensure copilot anthropic models have same reasoning effort model as copilot cli, also fix qwen incorrectly having variants (anomalyco#21212)

* tweak: adjust chat.params hook to allow altering of the maxOutputTokens (anomalyco#21220)

* tweak: move the max token exclusions to plugins  @rekram1-node (anomalyco#21225)

* fix: bump openrouter ai sdk pkg to fix openrouter issues (anomalyco#21242)

---------

Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Joscha Götzer <joscha.goetzer@gmail.com>
Co-authored-by: JosXa <info@josxa.dev>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Sebastian <hasta84@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: MC <mchen@cloudflare.com>
Co-authored-by: Valentin Vivaldi <valentin.vivaldi@etendo.software>
Co-authored-by: Aaron Zhu <139607425+aaron-he-zhu@users.noreply.github.com>
Co-authored-by: Aaron Zhu <aaron@Aarons-MacBook-Air.local>
Co-authored-by: dpuyosa <dpuyosa@users.noreply.github.com>
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
Co-authored-by: Noam Bressler <noamzbr@gmail.com>
Co-authored-by: Burak Yigit Kaya <byk@sentry.io>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Lenny Vaknine <lenny.vaknine@gmail.com>
Co-authored-by: Lenny Vaknine <lvaknine@gitlab.com>
Co-authored-by: ykswang <ykswang@users.noreply.github.com>
Co-authored-by: Juan Pablo Carranza Hurtado <52012198+jpcarranza94@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kevin Flansburg <kevin.flansburg@gmail.com>
Co-authored-by: Nate Williams <50088025+natewill@users.noreply.github.com>
Co-authored-by: David Hill <iamdavidhill@gmail.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: luanweslley77 <213105503+luanweslley77@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Yuxin Dong <yxdong9805@gmail.com>
Co-authored-by: dongyuxin <dongyuxin@dev.dongyuxin.msh-dev.svc.cluster.local>
Co-authored-by: Gautier DI FOLCO <gautier.difolco@gmail.com>
Co-authored-by: George Harker <george@georgeharker.com>
Co-authored-by: Corné Steenhuis <cornesteenhuis@hotmail.com>
Co-authored-by: Derek Barrera <derekbarrera@gmail.com>
balcsida pushed a commit to balcsida/opencode that referenced this pull request Apr 8, 2026
…ments (anomalyco#19178)

Co-authored-by: Lenny Vaknine <lvaknine@gitlab.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: macOS managed preferences support for enterprise MDM deployments

3 participants