Ad

Our DNA is written in Swift
Jump

Agentic Bash

The components I’ve been announcing recently certainly hinted at what I’m working on at the moment. A bash playground that lets me exercise SwiftBash, SwiftScript and SwiftPorts all tied together via ShellKit. There was one part that was on my private repo: the agentic harness and pure Swift wrappers for OpenAI APIs. I kept thinking that if I wanted to eventually make my Bash Playground public, then I needed to also have this final part on GitHub as well.

My mission statement for iBash is to have a universal app that gives me a virtual shell with a workspace in a file bundle that can do most of the scripting and editing work one would want to do mobile plus a coding agent that lives and works in those sandboxes.

This Coder agent is modelled after PI, with the exact same tools that PI has. Plus two additions: The latest OpenAI models are trained on editing files with the apply_patch tool, so I am using this instead of PI’s search-replace. Also there’s a sub_agent tool because this way Coder can do a bit of exploring with its own context window and can then come back with its findings to the main again without bloating the main context window.

The whole thing has gotten to the level where I can ask the agent to look around in this bash sandbox and try to find things that don’t work as they should. Then it can document the problems it finds in local markdown files and also use the gh tool (from SwiftPorts) to open new GitHub issues with the steps to reproduce the issue.

The I can have Claude iron out those wrinkles one by one. Take for example issue 46 from the screenshot above. My claude made PR 54 for that, after asking me via the Claude mobile app an architecture question. It resolved a review comment from codex, babysat the build until all platforms were green. At the end I could just merge the PR and delete the branch.

My hope is that if I keep doing this loop long enough that the surface of the bash commands in SwiftBash will become indistinguishable from real bash. An agentic self-improvement loop.

Let’s go over all my OSS projects and see what’s new.

SwiftText: I have been writing a markdown parser for no reason

swifttext had a hand-rolled markdown parser that I’d been patching for over a year. Every time something rendered weirdly in a PDF, I’d open MarkdownLexer.swift, find the edge case, add another regex, ship it.

Then last week I went looking at swift-markdown — Apple’s own markdown library, sitting at github.com/swiftlang/swift-markdown, used by DocC, perfectly maintained, with the full CommonMark + GFM AST surface I’d been trying to approximate by hand. I’d been ignoring it for years because I assumed it was DocC-internal.

It is not DocC-internal. It is a Swift package. You can just use it. I admit that I have a severe case of “not invented here” syndrome, but for Apple I make an exception…

SwiftText 1.1.9 replaces my parser with swift-markdown. Image alt-text now traverses nested inline content correctly (which my parser had been silently flattening), and the PDF renderer no longer forces a page break before every H1/H2 — so long technical documents stop wasting paper between every section.

There was one thing though that Apple’s markdown parser is still lacking: it doesn’t handle footnotes. But I am able to work around this with a pre- and post-processing step.

SwiftMCP: the */ bug, and a way to split servers

The bug-of-the-week award goes to a macro-expansion regression in SwiftMCP 1.4.5-original. I had generated documentation comments as /** ... */ blocks. The problem: if anyone (including me) wrote a docstring containing */ — most commonly "**/*" in a glob example — the doc block closed early and the rest spilled into raw Swift source, producing cascades of “unterminated string literal” errors with no obvious cause. Switching to /// line comments fixed it. The v1.4.5 tag was re-pointed at the fix; 1.4.6 widens the swift-syntax range so SwiftMCP coexists peacefully with SwiftScript.

More interesting structurally: @MCPExtension. You can now annotate extension YourServer in any file or even any other target with @MCPExtension, and the tools/resources/prompts defined there get aggregated into the primary @MCPServer. The generated Client surface is extended in lock-step, so callers see one client regardless of which target a given tool lives in. This was the architectural piece I needed for large MCP servers to stop ballooning into single-file monoliths.

SwiftMail: the Proton Mail Bridge incident

Someone tried to use SwiftMail with Proton Mail Bridge, which runs on a non-standard local port without traditional STARTTLS semantics, and discovered that my “infer TLS from the port number” heuristic guessed wrong. Fixing it properly meant exposing an explicit MailTransportSecurity enum — .automatic (the old guesser), .implicitTLS, .requiredSTARTTLS, .plaintext — so callers can override the inference.

The bigger fix was less glamorous but probably affects more people: envelope-date parsing. I’d been strict about RFC 5322 dates, which sounded reasonable until I tried to index a 15-year-old mailbox and watched thousands of “could not parse date” warnings scroll past. Real mail in the wild uses lowercase month names, omits time zones, embeds (UTC) parentheticals, and still uses obsolete US zone abbreviations like PST. SwiftMail 1.6.4 accepts all of those and stops complaining.

There’s also an MIME fix that I’m slightly embarrassed by: HTML emails with attachments were missing a closing boundary, so strict parsers were treating the attachment as still nested inside the HTML part. It rendered fine in Gmail and Apple Mail, but more pedantic clients would mangle it. Now fixed.Post: the downstream beneficiary

Post is the IMAP/SMTP daemon and CLI that’s basically my testbed for everything above. The v1.6.6 release shipped this week and is, by my own admission, the most boring release notes I’ve ever written: not a single line of Post’s own code changed. It’s pure dependency uptake.

But that uptake matters, because everything from SwiftMail’s better date parsing and MIME fix, SwiftMCP’s base64 binary correction, and SwiftText’s PDF page-break fix lands in post and postd automatically. post send now produces strictly-correct MIME for HTML-plus-attachment messages, post fetch stops complaining about 15-year-old date strings, post pdf no longer wastes paper before every heading, and the MCP daemon round-trips attachment bytes correctly. The point of the layered stack is that releases like this are allowed to be boring.

SwiftPorts: bringing the terminal to life

If the SwiftText story is about deleting a parser, the SwiftPorts story is about adding one back in a place it actually belonged.

GlamKit is the headline. It’s a pure-Swift port of Charm’s Glamour — markdown → ANSI rendering. The gh repo view and glab repo view commands now render the README right in your terminal, with syntax-highlighted code blocks, real GFM tables drawn with box-drawing characters, and properly themed headings. It uses swift-markdown for the parse step (see above), so the AST handling is solid; I only had to write the ANSI styling layer on top.

When I added the tool backing for Coder I found that PI is using rg aka ripgrep fd for find – so naturally I needed to add those as ports to the collection. And I found the grep subcommand was missing from the git port as well.

  • RipgrepKit — a pure-Swift port of BurntSushi/ripgrep. It now reads global gitignore, walks parent directories for repository ignore inheritance, and matches upstream rg behaviour close enough that I’ve been using it as my daily rg.
  • FdKit — pure-Swift port of sharkdp/fd with LS_COLORS support, match highlighting, the works.
  • git grep — finally added as a subcommand in the swiftgit port.

The speed at which we’re adding Swift ports of famous utilities to SwiftPorts is making my head spin. Much of the work also went into sandboxing support. Where libraries and utilities use FileManager.default for accessing files we have no way to block file access. So the only way is to sprinkle in a lot of permission checks before handing over a path.

SwiftBash: a hundred small things

SwiftBash got the most commits of any repo this fortnight, but very few of them are headline-worthy individually. It’s the thousand-paper-cuts phase where every script someone runs surfaces one more thing the interpreter handled subtly wrong.

The pattern that surprised me most: [ -t N ] (the test-if-fd-is-a-tty check that every colour-detection idiom relies on) and $'...' (ANSI-C quoting). I’d shipped without them because no script in my own collection used them, but every well-written CLI uses them to decide whether to emit colour. Once those landed, half my own ported tools suddenly started auto-detecting terminals correctly.

Other notable bits:

  • Bun’s JavaScriptCore on Linux, Windows, and Android. The JSC post talked about Apple platforms; SwiftBash 1.5-line picks up Bun’s prebuilt JavaScriptCore everywhere else, so swift-js now boots a full JS runtime on platforms where Apple doesn’t ship JSC at all.
  • less and more as in-process pager builtins. No external process, paging respects the sandbox.
  • Mode flags -e / -u / -x / -v — every “set -euo pipefail” script you’ve ever seen now actually behaves as intended.
  • compgen plus public completion sources, so embedders can hook tab-completion into their own UIs.
  • ${var:offset:length} with full nesting — the offset can itself be a substitution expression. Bash has been doing this for decades; my parser was bailing on the first nested brace.

ShellKit: the contract underneath

Most readers won’t touch ShellKit directly, but it’s the reason the other repos compose without circular dependencies.

ShellKit is the abstraction layer for “what is the shell environment, really?” — stdin/stdout/stderr sinks, environment, sandbox policy, network policy, the process table, and the binary catalog. The default is host-system passthrough; embedders override per-task. Every CLI tool I publish reaches host resources only through Shell.current, never FileManager or Foundation.Process directly. That’s what makes the same rg / fd / less / bash binary work the same way whether you run it from a real terminal, from inside SwiftBash, or from inside a sandboxed iOS app.

This fortnight ShellKit picked up the Shell.processLauncher subprocess primitive (so embedders can intercept process launches without monkey-patching), a cleaner Sandbox.Denial description that doesn’t leak absolute host paths into error messages, and a much more honest BinCatalog that knows about the script interpreters, git/gh/glab, the compression family, and the platform-specific binaries. CI now runs on macOS, iOS, Linux, Windows, and Android.

I also wanted to have proper support for compgen – the bash way of tab completion as well as authentic handling of $PATH. All commands now show in one of the standard bin folders or as builtin.

SwiftScript: a Swift interpreter that respects your sandbox

SwiftScript runs .swift files without swiftc. The recent work routes every Foundation IO door — file reads, network calls, process launch, identity lookups, exit — through Shell.current. Combined with SubprocessModule bridging import Subprocess to Shell.processLauncher, you can now embed a Swift interpreter in your app and have the script literally unable to escape your sandbox, even if it calls Subprocess.run.

It’s the same idea as JavaScriptCore from last post, applied to Swift itself. The most satisfying part: a third-party .swift file can import Foundation, write the most innocent-looking try Data(contentsOf:), and still hit your sandbox boundary instead of the user’s home directory.

SwiftAgents: opening the box

This is the big one for me. SwiftAgents is the LLM-and-agents SDK that’s been sitting inside a private repo called AgentCorp for the better part of two years, quietly doing the actual work behind almost everything else I ship. It’s now open on GitHub.

The shape of it: pure, beautiful Swift wrappers around the LLM APIs as they actually evolve. I started writing it when OpenAI still only had the Completions endpoint. Then I rewrote the surface for Assistants. Then for Responses. The current incarnation also covers Conversations, and the whole thing is structured as a Swift port of the OpenAI Agents SDK — modelled after their abstractions, but expressed in idiomatic Swift, with strong typing, async/await, and the kind of API surface that feels like Foundation rather than a transpiled Python shim.

There are two sample apps, one is Coder CLI – a basic coding agent for the terminal. The other is a real time voice agent I am tinkering with.

The realtime voice agent lets you phone with a snappy and smart voice agent that can edit files in the app’s documents folder via its built-in coding agent. It transcribes the conversation and it has the ability to ask OpenClaw via the chat completions API. This was the test bed for adding realtime session support to SwiftAgents.

Next Steps

Honestly, I don’t know if the realtime agent even works at this point, the first big step was to get it all onto GitHub. Next I’ll need to make sure that it is still working with the changed and unified dependencies. Once I have it working again, I’ll be sure to show it off in a video.

Also iBash is growing in leaps and bounds and I am looking forward to giving you a tour of it soon. I’m hesitant to open source it because I might want to put it on the app store first. Also maybe do an open beta with Testflight. That might be fun and help me find more issues.

And then there are some additions to the LLM APIs that I would like to do in my SDK. OpenAI recently added the capability of calling their APIs via web sockets, so probably that. Happy to hear your thoughts and ideas on any of the above!


Categories: Administrative

Comments are closed.