swift-subprocess: a concurrency-first API for launching child processes
Subprocess is a cross-platform package for spawning processes in Swift.
At a glance
- What is it?
- swift-subprocess is an Apache-2.0 Swift package from the Swift project for spawning subprocesses without Foundation, with separate input, output and error streams and an async sequence model for reading output as it arrives.
- Who is it for?
- swift-subprocess fits Swift projects on macOS and Linux that want structured control over child process streams and are prepared to track a toolchain floor that moves with minor releases. It does not fit a codebase that must build against an older Swift version, or one that needs Foundation-specific conveniences without opting into the SubprocessFoundation trait.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A subprocess API that assumes Swift concurrency from the start
swift-subprocess is a Swift package for launching child processes, and its distinguishing feature is that it was written with Swift concurrency in mind rather than wrapped around an older blocking model. The project describes it as a cross-platform package for spawning processes in Swift, built from the ground up with concurrency. The audience is Swift developers who need to run a command, capture what it printed, feed it input, or watch its output live, and who would otherwise reach for Foundation's Process type and manage pipes by hand. The package is hosted under the swiftlang organisation and is licensed under Apache-2.0. Its scope is deliberately narrow: it starts processes, wires their standard streams, and gives you back an execution result. It does not try to be a job scheduler, a shell, or a general process management framework, and the README does not document any of those.
Input, output and error are three independent parameters
The design decision that shapes the whole API is that standard input, standard output and standard error are configured separately, and each one can be handled in a different way. The README lays out the mapping. Writing to standard input from the closure means passing input: .inputWriter and reading from execution.standardInputWriter. Streaming standard output means passing output: .sequence and iterating execution.standardOutput. Streaming standard error means passing error: .sequence and iterating execution.standardError. Because the three are independent, a single call can stream standard output line by line while collecting standard error as a captured string. The simplest form collects output and returns everything at the end:
import Subprocess
let result = try await run(.name("ls"), output: .string(limit: 4096))
print(result.processIdentifier) // e.g. 1234
print(result.terminationStatus) // e.g. exited(0)
print(result.standardOutput) // e.g. Optional("LICENSE\nPackage.swift\n...")The result is an ExecutionResult carrying the process identifier, the termination status and the collected output.
Adding the package to Package.swift and the target
Using the package in a SwiftPM project takes two edits. First the package dependency in Package.swift:
dependencies: [
.package(
url: "https://github.com/swiftlang/swift-subprocess.git",
.upToNextMinor(from: "0.4.0")
)
]Then the product is added to the target that needs it:
.target(
name: "MyTarget",
dependencies: [
.product(name: "Subprocess", package: "swift-subprocess")
]
)One extra knob is worth knowing about. The package exposes a single package trait named SubprocessFoundation, which pulls in a dependency on Foundation and adds extensions on Foundation types such as Data. The trait is enabled by default, so if you want the core package without Foundation you have to switch it off deliberately. The README does not document further configuration options, platform conditionals or environment variables for the package.
The closure is the only place the execution values are valid
The most important rule in the README is a caution about lifetime. The Execution, SubprocessOutputSequence and StandardInputWriter values are valid only for the duration of the closure that runs while the subprocess is active, and the documentation warns against letting them escape it. This is the price of the streaming design, and it is a real constraint rather than a formality: a value that outlives the closure has no process behind it any more. The streaming example shows the shape. You pick .path or .name to resolve the executable, pass arguments, choose the input, output and error modes, and then consume the streams inside the closure:
import Subprocess
let result = try await run(
.path("/usr/bin/tail"),
arguments: ["-f", "/path/to/nginx.log"],
input: .none,
output: .sequence,
error: .discarded
) { execution in
for try await line in execution.standardOutput.strings() {
if line.contains("500") {
// Oh no, 500 error
}
}
}The closure-based call still returns an ExecutionResult, where the closure's own return value is available as result.closureResult and the exit status as result.terminationStatus.
Output arrives as Buffers, and strings() is the intended reader
In the closure-based API, output streams are delivered as a SubprocessOutputSequence, an asynchronous sequence of Buffer values. Each Buffer exposes its bytes through withUnsafeBytes(_:) or through a bytes property typed as RawSpan, so the raw data is always reachable. The README is explicit that the preferred way to turn a Buffer into text is to read the output line by line with .strings(), and it notes that an encoding can be specified optionally. That is a sensible default because line-oriented reading is what log tailing and interactive commands actually need, but it also means a consumer that wants partial lines or raw bytes has to drop to the span-level API. The captured-string mode, shown earlier with .string(limit: 4096), takes an explicit limit, which makes truncation a decision you make at the call site rather than an accident. Errors are separate in the same way, and .discarded is available when a stream is not wanted at all.
Minor releases can raise the Swift toolchain floor
The package manages its own compatibility policy, and it is stricter than many Swift packages. The README states the intent directly: the project wants to adopt Swift language and toolchain improvements quickly, so from time to time a new version of the package requires clients to move to a more recent toolchain. Patch releases will not raise the required toolchain, but a minor release may. The table makes the floors explicit. The 0.1.x through 0.4.x lines require Swift 6.1 and Xcode 16.3 or later, while 0.5.x and the 1.0.x line require Swift 6.2 and Xcode 26, as does the main branch. There is a subtlety on Darwin: the Xcode column there states an SDK requirement as well as a toolchain requirement, because Foundation and the other SDK modules come from the active Xcode, so a toolchain installed from swift.org still needs the listed Xcode selected through xcode-select or DEVELOPER_DIR. A build that fails for reasons that look unrelated to your code is often this mismatch.
Apache-2.0 with a fast release cadence since 1.0
The package is released under Apache-2.0, with the licence file at the root of the repository and a licence header template and licenceignore file alongside it, which is the convention for a package whose sources all carry a header. Version 1.0.0 arrived on 2026-08-04, followed by 1.0.1 on 2026-10-09 and 1.0.2 on 2026-10-16, so the 1.0 line has been getting patch releases since it stabilised. The last push was on 2026-10-16, the same day as the 1.0.2 tag. For a project that has reached 1.0 but explicitly reserves the right to raise its toolchain floor in a minor release, the practical cost of upgrading is not the API but the Swift version you are forced onto. The repository layout is conventional for a Swift package: Sources, Tests, Package.swift, a CMakeLists.txt, a cmake directory and scripts, plus a .swift-version file and a .swift-format configuration.
Editorial conclusion
swift-subprocess fits Swift projects on macOS and Linux that want structured control over child process streams and are prepared to track a toolchain floor that moves with minor releases. It does not fit a codebase that must build against an older Swift version, or one that needs Foundation-specific conveniences without opting into the SubprocessFoundation trait. Before adopting it, check the minimum Swift and Xcode version your target requires against the release table, confirm that the platform you deploy on is covered by the package, and decide whether the closure-scoped lifetime of Execution values matches how your code is structured.
Frequently asked questions
How do I add swift-subprocess to a SwiftPM project?
Add the package URL with a version requirement in the dependencies array of Package.swift, then add the Subprocess product from the swift-subprocess package to the dependencies of the target that needs it. The README gives .upToNextMinor(from: "0.4.0") as the requirement example.
What Swift and Xcode versions does swift-subprocess require?
Releases 0.1.x through 0.4.x need Swift 6.1 and Xcode 16.3 or later, while 0.5.x and 1.0.x need Swift 6.2 and Xcode 26. Patch releases do not raise the requirement, but a minor release may, so the floor moves with each new minor version.
How do I stream standard output line by line in swift-subprocess?
Pass output: .sequence and error: .discarded, then iterate execution.standardOutput.strings() inside the run closure. The Execution, SubprocessOutputSequence and StandardInputWriter values are only valid while that closure is running and should not escape it.
Does swift-subprocess depend on Foundation?
The package offers one package trait, SubprocessFoundation, which adds a dependency on Foundation and provides extensions on Foundation types such as Data. That trait is enabled by default, so the Foundation-free core requires turning it off explicitly.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/swiftlang-swift-subprocess)Was this useful?