A pure-Swift, sandboxed bash interpreter that drops into any Mac or
iOS binary. No Process, no fork, no exec — every command is a
registered Swift type, every byte streams through async channels, and
every host-touching axis (filesystem, network, processes, identity)
is virtualised by default.
The goal: be the local code interpreter for an LLM agent. Hand the agent a workspace folder, hand it the network endpoints it's allowed to call, and let it write and run bash scripts that manipulate files, parse data, and fetch documents — all without ever touching anything outside its sandbox. Same shell runs equally well on a server, a Mac CLI tool, an iOS App Sandbox extension, or a Swift Playground.
import BashInterpreter
import BashCommandKit
let shell = Shell() // sandbox-by-default identity
shell.registerStandardCommands() // ls, cat, grep, sed, find, …
try await shell.run("""
for f in *.txt; do
echo "$(basename "$f" .txt): $(wc -l < "$f") lines"
done | sort -k2 -n
""")| Product | Status | What it does |
|---|---|---|
BashSyntax |
Available | Parse bash into a typed AST. Smart tokeniser for shell-aware splitting. |
BashInterpreter |
Available | Execute the AST in-process. Streaming pipelines, full bash 4.x semantics. |
BashCommandKit |
Available | Catalog of ls/cat/grep/sed/find/curl/… built on Swift Argument Parser. |
SwiftJSCore |
Available on macOS / iOS / tvOS / watchOS / visionOS / Linux / Android | Node-style JavaScript runtime on JavaScriptCore. require, ESM import, node:fs/path/os/crypto/zlib/child_process/…, fetch, Buffer, timers, AbortController, WebAssembly. |
BashSwiftScript |
Available | #!/usr/bin/env swift-script shebang dispatch via Cocoanetics' SwiftScript tree-walking interpreter. IO / sandbox / identity flow through ShellKit, so a script honors the bash sandbox the same way gh/jq/etc. do. |
swift-bash (CLI) |
Available | exec and parse subcommands; sandbox flags for confined execution. Auto-registers SwiftScript so ./hello.swift runs in-process. |
swift-js (CLI) |
Available on macOS / iOS / tvOS / watchOS / visionOS / Linux / Android | Drop-in for node — swift-js install symlinks node/bun so existing #!/usr/bin/env node scripts run unchanged. |
- BashSyntax — AST model, parser, tokeniser, visitor protocol.
- BashInterpreter — execution model, streams, custom commands, built-ins, bash 4.x semantics.
- BashCommandKit — every shipped
ls/cat/grep-style command + how to add your own typed ones. - SwiftJS — Node-style JavaScript runtime on
JavaScriptCore. Embeddable +
swift-jsCLI shadow fornode/bun. One Swift source tree, zero per-platform branches in the runtime — every file talks to JSC's stable C API directly. Apple platforms use the systemJavaScriptCore.framework; Linux and Android link the engine from Bun's prebuilt JSC archive via theCJavaScriptCoreC-API target. CI runs the full SwiftJSCore test suite on every supported platform. - SwiftScript —
#!/usr/bin/env swift-scriptshebang dispatch routing through SwiftScript's tree-walking interpreter. Wires output / stdin / sandbox / network / identity through the bash shell's ShellKit context. - Sandboxing — the four virtualisation axes
(filesystem, network, processes, identity) and the
--sandboxflag. - Networking —
curl, the URL allow-list, SSRF defenses. - Virtual /bin and /usr/bin — how
ls /binreflects the live command registry instead of the host's binaries. - CLI —
swift-bash parseandswift-bash exec, with examples. - Bash version conformance — every
feature where SwiftBash diverges from macOS-shipped
/bin/bash 3.2.
Because the runtime targets are places where you can't fork:
- iOS apps — any process spawn is blocked by App Sandbox.
- macOS App Sandbox — same constraint.
- Swift Playgrounds — pure interpreter only.
- Server-side Swift — embedding a shell shouldn't require pulling
in a separate
bashbinary or worrying about$PATHdifferences.
Every command is a Command Swift type; pipelines are
AsyncStream<Data> channels; the FS is a FileSystem protocol you
swap implementations of. Nothing leaves the process unless an embedder
explicitly says "yes, run this network request" or "yes, surface my
real username."
A freshly-constructed Shell() already leaks nothing about the host:
$ echo 'whoami; hostname; ls /Users; cat /etc/passwd' \
| swift-bash exec --sandbox /tmp/work /dev/stdin
user
sandbox
ls: /Users: No such file or directory
cat: /etc/passwd: No such file or directoryTo opt in:
shell.hostInfo = .real() // real whoami
shell.networkConfig = NetworkConfig( // allow API calls
allowedURLPrefixes: ["https://api.example.com/"])
shell.fileSystem = RealFileSystem() // real diskEach axis is independent. Read Sandboxing for the threat model and the complete picture.
SwiftBash is an interpreter, not a process host — so a few things behave differently from a native shell. None of these are bugs:
- Commands are in-process Swift builtins, not executables. Reading
/bin/bashor/usr/bin/curlyields a one-line marker, not a Mach-O/ELF — the binary doesn't exist;ls /binreflects the live command registry. See Virtual /bin and /usr/bin. - Outbound network is denied by default —
curl https://example.comfails with(7) Network access denieduntil an embedder installs aNetworkConfigallow-list (--allow-url …on the CLI). See Networking. - The filesystem is a chroot-style mount table.
/is the read-only sandbox root; the workspace (/batchunder--sandbox,/homein document apps) is writable;/tmpis writable scratch on a per-instance dir under the host temp dir (removed when the script ends); everything else returns No such file or directory.mountprints the table. - Identity is synthetic by default —
whoami→user,id→uid=1000(user) gid=1000(users), andunamea Darwin-flavoured kernel string over a generic Unix layout.stat/ls -lreport this same virtual identity, never the host's ids;hostInfo = .real()opts in. - The process table is virtual —
ps/pgrep/killsee only this shell and its&background jobs, never host processes. - Most common GNU/BSD options work; gaps report on stderr. One
structural note: ArgumentParser-backed commands take the separated
short-option form (
-l 64), not the attached one (-l64) — commands that need the attached form scan their own argv.
See Sandboxing for the full model.
.package(url: "https://github.com/Cocoanetics/SwiftBash",
.upToNextMinor(from: "0.1.0")),SwiftBash is 0.x: a minor release may change API, a patch release
does not, so pin the minor. The whole ShellKit family (ShellKit,
SwiftPorts, SwiftScript, SQLiteKit) is versioned the same way and
moves minors together, so one upToNextMinor here is enough to
get a consistent graph. Then depend on the products you want:
.target(name: "YourTarget", dependencies: [
.product(name: "BashInterpreter", package: "SwiftBash"),
.product(name: "BashCommandKit", package: "SwiftBash"),
// .product(name: "BashSyntax", package: "SwiftBash"), // AST only
]),MIT. See LICENSE.
