Bash.swift is an in-process, stateful shell for Swift apps. It is inspired by just-bash. Commands runs inside Swift instead of spawning host shell processes.
You create a BashSession, run shell command strings, and get structured stdout, stderr, and exitCode results back. Session state persists across runs, including the working directory, environment, history, and registered built-ins.
Bash.swift should be treated as beta software. It is practical for app and agent workflows, but it is not a hardened isolation boundary and it is not a drop-in replacement for a real system shell. APIs are being actively experimented with and deployed. Ensure you lock to a specific commit or version tag if you plan to do any work utilizing this library.
Bash.swift is built for app and agent workflows that need shell-like behavior without subprocess management.
It provides:
- Stateful shell sessions (
cd,export,history, shell functions) - Real filesystem side effects under a controlled root
- In-process built-in commands implemented in Swift
- Practical shell syntax support for pipelines, redirection, chaining, background jobs, and simple scripting
Add Bash to your Package.swift:
// Package.swift
.dependencies: [
.package(url: "https://github.com/velos/Bash.swift.git", from: "0.1.0")
],
.targets: [
.target(
name: "YourTarget",
dependencies: ["Bash"]
)
]Traits are the way to compile optional toolsets into the package. Traits are default-off in Bash.swift, so add them on the package dependency when you need optional command sets:
.dependencies: [
.package(
url: "https://github.com/velos/Bash.swift.git",
from: "0.1.0",
traits: ["Git", "Python", "SQLite", "Secrets"]
)
]Use Xcode 26.4 or newer if you want to configure package traits from the Xcode UI.
- Open your app or package project in Xcode.
- Select the project in the navigator, then open the
Package Dependenciestab. - Add
https://github.com/velos/Bash.swift.git. - Select the
Bash.swiftpackage dependency and choose the traits you want enabled.
The traits shown in Xcode map directly to the SwiftPM traits in Package.swift. Enable only the features you need:
GitPythonSQLiteSecrets
Notes:
Bash.swiftdepends on theWorkspacepackage for the reusable filesystem layer.Bashreexports the Workspace filesystem types,BashCore,BashTools, and any trait-enabled feature APIs, so downstream code only needsimport Bash.- The
Pythontrait uses a prebuiltCPython.xcframeworkbinary target. - The
Gittrait uses a prebuiltClibgit2.xcframeworkbinary target.
Supported package platforms:
- macOS 13+
- iOS 16+
- tvOS 16+
- watchOS 9+
import Bash
import Foundation
let root = URL(fileURLWithPath: "/tmp/bash-session", isDirectory: true)
let session = try await BashSession(rootDirectory: root)
_ = await session.run("touch file.txt")
let ls = await session.run("ls")
print(ls.stdoutString) // file.txt
let piped = await session.run("echo hello | tee out.txt > copy.txt")
print(piped.exitCode) // 0For isolated per-run overrides without mutating the session's persisted shell state:
let scoped = await session.run(
"pwd && echo $MODE",
options: RunOptions(
environment: ["MODE": "preview"],
currentDirectory: "/tmp"
)
)Git, Python, and SQLite are compiled in via traits and auto-register when BashSession starts. Secrets is also compiled in via a trait, but it stays disabled until you explicitly provide a secrets provider at runtime.
With traits: ["Git", "Python", "SQLite"] on the package dependency:
import Bash
let session = try await BashSession(rootDirectory: root)
_ = await session.run("git init")
let sql = await session.run("sqlite3 :memory: \"select 1;\"")
let py = await session.run("python3 -c \"print('hi')\"")
print(sql.stdoutString) // 1
print(py.stdoutString) // hiThe Python trait embeds CPython directly. The current prebuilt runtime is available on macOS. Other Apple platforms still compile, but runtime execution returns unavailable errors. Filesystem access stays inside the shell's configured FileSystem, and escape APIs such as subprocess, ctypes, and os.system are intentionally blocked. Maintainer notes for the broader Apple runtime plan live in docs/cpython-apple-runtime.md.
With traits: ["Secrets"] on the package dependency:
import Bash
let session = try await BashSession(rootDirectory: root)
let provider = AppleKeychainSecretsProvider()
await session.enableSecrets(provider: provider)
let ref = await session.run(
"secrets put --service app --account api",
stdin: Data("token".utf8)
)The Secrets trait uses provider-owned opaque secretref:... references. secrets get --reveal is explicit, and .resolveAndRedact or .strict policies keep plaintext out of caller-visible output by default.
Bash sits on top of a reusable Workspace package. If you only need filesystem and workspace tooling, use Workspace directly instead of BashSession.
Example:
import Workspace
let filesystem = PermissionedFileSystem(
base: try OverlayFilesystem(rootDirectory: workspaceRoot),
authorizer: PermissionAuthorizer { request in
switch request.operation {
case .readFile, .listDirectory, .stat:
return .allowForSession
default:
return .deny(message: "write access denied")
}
}
)
let workspace = Workspace(filesystem: filesystem)
let tree = try await workspace.summarizeTree("/workspace", maxDepth: 2)Primary entry point:
public final actor BashSession {
public init(rootDirectory: URL, options: SessionOptions = .init()) async throws
public init(options: SessionOptions = .init()) async throws
public func run(_ commandLine: String, stdin: Data = Data()) async -> CommandResult
public func run(_ commandLine: String, options: RunOptions) async -> CommandResult
public func register(_ command: any BuiltinCommand.Type) async
}When built with the Secrets trait, BashSession also exposes enableSecrets(provider:policy:redactor:).
High-level types:
CommandResult:stdout,stderr,exitCode, plus string helpersRunOptions: per-runstdin, environment overrides, temporarycwd, execution limits, and cancellation probeExecutionLimits: caps command count, function depth, loop iterations, command substitution depth, and optional wall-clock durationSessionOptions: filesystem, layout, initial environment, globbing, history length, network policy, execution limits, permission callback, and secret policyShellPermissionRequest/ShellPermissionDecision: shell-facing permission callback typesShellNetworkPolicy: built-in outbound network policy
Practical behavior:
BashSession.initcan throw during setuprunalways returns aCommandResult, including parser/runtime faults- Unknown commands return exit code
127 - Parser/runtime faults use exit code
2 maxWallClockDurationfailures use exit code124- Cancellation uses exit code
130
Bash.swift is a practical execution environment, not a hardened sandbox.
Current hardening layers include:
- Root-jail filesystem implementations plus null-byte path rejection
- Optional permission callbacks for filesystem and network access
ShellNetworkPolicywith default-off HTTP(S), host allowlists, URL-prefix allowlists, and private-range blocking- Execution budgets through
ExecutionLimits - Strict
Pythontrait shims that block process and FFI escape APIs - Secret-reference resolution and redaction policies
Important notes:
- Outbound HTTP(S) is disabled by default
permissionHandlerapplies after the built-in network policy passes- Permission wait time is excluded from
timeoutand run-level wall-clock accounting curl/wget,git clone, andPythontrait socket connections share the same network policy pathdata:URLs and jailedfile:URLs do not trigger outbound network checks
Filesystems available via Workspace:
ReadWriteFilesystem: rooted real disk I/OInMemoryFilesystem: fully in-memory treeOverlayFilesystem: snapshots an on-disk root into memory; later writes stay in memoryMountableFilesystem: composes multiple filesystems under virtual mount pointsSandboxFilesystem: container-root chooser (documents,caches,temporary, app group, custom URL)SecurityScopedFilesystem: security-scoped URL or bookmark-backed root
Behavior guarantees:
- All shell-visible paths are scoped to the configured filesystem root
ReadWriteFilesystemblocks symlink escapes outside the root- Filesystem implementations reject paths containing null bytes
- Built-in command stubs are created under
/binand/usr/binfor unix-like layouts - Unsupported platform features surface as runtime unsupported errors from
BashorWorkspace
Rootless session example:
let options = SessionOptions(filesystem: InMemoryFilesystem(), layout: .unixLike)
let session = try await BashSession(options: options)Supported shell features include:
- Quoting and escaping
- Pipes
- Redirections:
>,>>,<,<<,<<-,2>,2>&1 - Chaining:
&&,||,; - Background execution with
jobs,fg,wait,ps,kill - Command substitution:
$(...) - Variables and default expansion:
$VAR,${VAR},${VAR:-default},$! - Globbing
- Here-documents
- Functions and
local if/elif/elsewhile,until,for ... in ..., and C-stylefor ((...))- Path-like command invocation such as
/bin/ls
Not supported:
- A full bash or POSIX shell grammar
- Host subprocess execution for ordinary commands
- Full TTY semantics or real OS job control
- Many advanced bash compatibility edge cases
All built-ins support --help, and most also support -h.
Core built-in coverage includes:
- File operations:
cat,cp,ln,ls,mkdir,mv,readlink,rm,rmdir,stat,touch,chmod,file,tree,diff - Text processing:
grep,rg,head,tail,wc,sort,uniq,cut,tr,awk,sed,xargs,printf,base64,sha256sum,sha1sum,md5sum - Data tools:
jq,yq,xan - Compression and archives:
gzip,gunzip,zcat,zip,unzip,tar - Navigation and environment:
basename,cd,dirname,du,echo,env,export,find,printenv,pwd,tee - Utilities:
clear,date,false,fg,help,history,jobs,kill,ps,seq,sleep,time,timeout,true,wait,whoami,which - Network commands:
curl,wget,html-to-markdown
Optional command sets:
sqlite3via theSQLitetraitpython3/pythonvia thePythontraitgitvia theGittraitsecrets/secretvia theSecretstrait afterenableSecrets(...)
Run the test suite with:
swift testTrait-specific coverage is available through SwiftPM traits, for example:
swift test --disable-default-traits
swift test --traits Git,Python,SQLite,SecretsThe repository includes parser, filesystem, integration, command coverage, and trait-gated feature tests.
