# How the Container Plugin System Works in Apple's Container Project

> Discover Apple's container plugin system. Learn how this file-based extension mechanism adds CLI tools and background services to the Container daemon without recompiling.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: internals
- Published: 2026-06-11

---

**The container plugin system is a file-based extension mechanism that lets developers add CLI tools or background services to the Container daemon without recompiling the core binary.**

The `apple/container` repository implements a modular plugin architecture that discovers extensions from disk at runtime. Each plugin is a self-contained directory bundling an executable binary, declarative configuration, and optional resources, enabling seamless integration with both the command-line interface and the daemon's service layer.

## Plugin Directory Structure and Discovery

Every plugin resides in a dedicated directory under a *plugin root* path, such as `<app-root>/user-plugins` or the system-wide directory returned by `PluginLoader.userPluginsDir`. A valid plugin directory contains:

- `bin/<plugin-name>` – The executable implementing the plugin functionality.
- [`config.toml`](https://github.com/apple/container/blob/main/config.toml) or [`config.json`](https://github.com/apple/container/blob/main/config.json) – Declarative metadata describing the plugin's abstract, author, and optional services.
- `resources/` – Optional directory for runtime assets the plugin can access.

Discovery is orchestrated by `PluginLoader.findPlugins()` in [`Sources/ContainerPlugin/PluginLoader.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginLoader.swift) (lines 90-122). This method walks each URL in `pluginDirectories`, resolves symbolic links, and attempts to construct `Plugin` objects via registered `PluginFactory` implementations. If two plugins share the same name, the loader implements **shadowing** where the first discovered plugin wins; subsequent duplicates are logged and skipped to avoid duplicate launchd labels (lines 149-158).

## Configuration Schema and Plugin Types

`PluginConfig` in [`Sources/ContainerPlugin/PluginConfig.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginConfig.swift) (lines 21-98) defines the schema that configuration files must follow. The system distinguishes between two plugin categories through the `servicesConfig` field:

- **CLI-only plugins** – Set `servicesConfig` to `nil`. These are exposed only in the daemon's help text. The convenience property `isCLI` (lines 104-107) identifies these plugins during CLI construction.
- **Service plugins** – Provide a `ServicesConfig` containing one or more `Service` entries, each specifying a `DaemonPluginType` (`runtime`, `network`, `core`, or `auxiliary`).

The configuration is decoded using either `TOMLDecoder` or `JSONDecoder`, allowing plugin authors to choose between [`config.toml`](https://github.com/apple/container/blob/main/config.toml) and [`config.json`](https://github.com/apple/container/blob/main/config.json) formats.

## Launchd Registration and Mach Services

For plugins that expose services, `PluginLoader.registerWithLaunchd` (lines 110-154) manages integration with the system's service manager. The registration process:

1. **Environment filtering** – The `filterEnvironment` method (lines 68-75) strips all environment variables except those prefixed with `CONTAINER_` or known proxy variables, preventing leakage of unrelated system settings.
2. **Mach service naming** – `Plugin.getMachServices` in [`Sources/ContainerPlugin/Plugin.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/Plugin.swift) (lines 59-74) generates unique Mach service names following the pattern `com.apple.container.<type>.<plugin-name>[.<instanceId>]`.
3. **Plist creation** – A `LaunchPlist` is written to `plugin-state/<plugin-name>/service.plist` containing the binary path, filtered environment, and Mach service names, then registered with the system `ServiceManager`.

## CLI Integration and Help Text

When the daemon runs with `--help`, `PluginLoader.alterCLIHelpText` (lines 70-88) dynamically appends a **"PLUGINS:"** section listing all CLI-only plugins. The display format is generated by `Plugin.helpText(padding:)`, ensuring that third-party extensions appear alongside built-in commands without modifying the source code.

## Factory Abstraction

The `PluginFactory` protocol (defined in [`Sources/ContainerPlugin/PluginFactory.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginFactory.swift)) abstracts the logic required to locate a plugin's binary, configuration, and resources. Concrete implementations like `DefaultPluginFactory` and `AppBundlePluginFactory` handle different packaging formats. The `PluginLoader` sequentially tries each factory until one successfully constructs a `Plugin` object, allowing multiple plugin formats to coexist in the same installation.

## Summary

- The **container plugin system** uses file-based discovery in designated directories to load extensions without recompiling the daemon.
- Each plugin requires a `bin/<plugin-name>` executable and a [`config.toml`](https://github.com/apple/container/blob/main/config.toml) or [`config.json`](https://github.com/apple/container/blob/main/config.json) metadata file describing its capabilities.
- Service plugins integrate with **launchd** via `PluginLoader.registerWithLaunchd`, receiving filtered environment variables and unique Mach service names generated by `Plugin.getMachServices`.
- CLI-only plugins automatically appear in the daemon's help text via `PluginLoader.alterCLIHelpText`.
- The `PluginFactory` abstraction allows the loader to support multiple plugin packaging formats through a common interface.

## Frequently Asked Questions

### What file format should I use for plugin configuration?

The system supports both **TOML** ([`config.toml`](https://github.com/apple/container/blob/main/config.toml)) and **JSON** ([`config.json`](https://github.com/apple/container/blob/main/config.json)), decoded via `TOMLDecoder` and `JSONDecoder` respectively. While both formats work, TOML is generally preferred for human-readable configuration files due to its cleaner syntax for multi-line strings and comments.

### How does the container plugin system handle naming conflicts?

When `PluginLoader.findPlugins()` in [`Sources/ContainerPlugin/PluginLoader.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPlugin/PluginLoader.swift) encounters duplicate plugin names during directory scanning, it implements shadowing where the first plugin discovered wins. Subsequent duplicates are logged and skipped to prevent duplicate launchd labels and service registration conflicts.

### Can a plugin access environment variables from the Container daemon?

Yes, but with restrictions. The `filterEnvironment` method (lines 68-75) in [`PluginLoader.swift`](https://github.com/apple/container/blob/main/PluginLoader.swift) only passes environment variables prefixed with `CONTAINER_` or known proxy variables to the plugin process. This prevents sensitive system environment variables from leaking into third-party plugin binaries.

### What is the difference between CLI-only and service plugins?

**CLI-only plugins** have `servicesConfig` set to `nil` in their configuration and are invoked on-demand through the command-line interface. **Service plugins** define long-running daemons in their `servicesConfig` that register with launchd and expose Mach services for XPC communication, enabling persistent background functionality.