What is a command plugin, what can it touch, and what does the permission model look like?
Predict first
What does the plugin report without and with the write permission flag?
// === Package.swift ===
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "App",
targets: [
.executableTarget(name: "App"),
.plugin(name: "Notes", capability: .command(intent: .custom(verb: "notes", description: "Writes notes.txt"))),
]
)
// === Sources/App/main.swift ===
print("app")
// === Plugins/Notes/plugin.swift ===
import PackagePlugin
import Foundation
@main struct Notes: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
for t in context.package.targets where t is SourceModuleTarget { print("target:", t.name) }
do {
try "x".write(to: context.package.directoryURL.appending(path: "notes.txt"), atomically: true, encoding: .utf8)
print("wrote notes.txt")
} catch { print("write failed:", (error as NSError).code) }
}
}
// === run.sh ===
echo "default: $(swift package notes 2>&1 | tail -2 | tr '\n' ' ')"
echo "allowed: $(swift package --allow-writing-to-package-directory notes 2>&1 | tail -2 | tr '\n' ' ')"
echo "file exists: $(ls notes.txt)"
- default: write failed 513; with the flag: wrote notes.txt and the file exists
- both runs write the file
- both runs fail
Mental model. A command plugin is a contractor you invite in: they ask permission at the door for each room they need (write the repository, use the network) and cannot wander elsewhere.
A command plugin is a package-provided command you run on demand: swift package <verb> (or Xcode’s context menu on a package). It conforms to CommandPlugin, receives the package model (targets, files, tools) and may do anything a program can: format code, regenerate sources, update a baseline, produce documentation. Because that is powerful, it is sandboxed on macOS and must declare what it needs: .command(intent: …, permissions: [.writeToPackageDirectory(reason: "…")]). In the check, a plugin that tries to write notes.txt into the package directory fails with error code 513 (NSFileWriteNoPermissionError, the sandbox denial) and the same command succeeds with --allow-writing-to-package-directory, leaving the file on disk. Network access is a separate permission (.allowNetworkConnections) and is denied by default; Xcode shows the declared reason to the user for approval.
Use a command plugin for work that changes the repository on request (reformat, relicense, regenerate a checked-in file), a build tool plugin (Q627) for work the build needs every time. Design points: keep the list of permissions minimal and give honest reasons, since reviewers read them; run it from CI with the explicit allow flag instead of disabling the sandbox; make output deterministic so the diff is reviewable. The sandbox is a macOS feature; the check does not say what other platforms do, and plugins that run external tools still need those tools installed (context.tool(named:) finds tools from a plugin’s dependencies and from the toolchain). Plugin products can be exposed to other packages, which is how formatting and linting commands are shared.
The sandbox denies writes to the package directory until you allow them.
Common traps
- Granting write access in the manifest “just in case”. The permission list is the review surface.
- Running unreviewed plugins with the sandbox disabled. They then act with your user’s rights.
They might follow up with
- Why use a plugin over a shell script in the repo? Typed access to the package model, a declared sandbox, and one command that works in CLI and Xcode.
- How does CI run one? With the explicit allow flags for the permissions it declared, never a blanket bypass.
Related questions
- 16.8.5 What is a build tool plugin, how does it generate code, and what does it replace?
- 16.7.4 Why does SwiftPM refuse a dependency that uses unsafeFlags, and what should you do instead?
- 16.1.1 What are targets, products and modules in a Swift package, and what does a first Package.swift look like?
- 16.6.1 A CI pipeline takes hours on a large modular app. How do you analyse and cut it: affected builds, caching…
← What is a build tool plugin, how does it generate code, and what does…