The gap that kept bothering me was this: I am talking to an assistant that lives on my Mac, and it cannot see my Mac.
It cannot open a file. It cannot check what git branch I am on. It cannot create an apple reminder that shows up on my phone. It cannot tell me what is in my Downloads folder. Everything requires me to copy something in, or copy something out, or do the thing myself while Iris watches.
That is not a 2100 century AI assistant (yes, you read it right - the year 2100 lol). That is a very capable chatbot with a wall around it.
So I removed the wall.
The isolation problem
Iris runs in Docker. That is correct and I am not changing it. But Docker containers are isolated from the host by design. They cannot call osascript. They cannot read files from the home directory. They cannot send a macOS notification. The container and the Mac are separate environments that do not know about each other.
The standard solution to this is a host bridge: a small server running natively on the Mac that the containers can reach. Docker provides host.docker.internal specifically for this purpose. The container makes an HTTP call to that address, the Mac answers.
So that is what I built (Also my mate, Claudia). A lightweight Node.js server, no npm dependencies, running on port SET_PORT. Bearer token authenticated. The containers call it, the Mac does the work.
What it can do
On the file side: read files with line-range pagination, write files, list directories, search file contents with ripgrep (falling back to grep if ripgrep is not installed), and analyse a file's metadata, type, and preview regardless of format.
On the Apple side: full Notes CRUD (list, get, create, update, search), full Reminders CRUD (lists, list, create, update, complete, delete), and native macOS notification banners.
Shell access is gated behind a command allowlist. The model can run git, gh, brew, curl, and a handful of other trusted binaries. It cannot run arbitrary commands.
One detail that matters: when Iris creates a reminder, it now auto-syncs to Apple Reminders. That happens via a model observer on the Reminders model. Every Iris reminder also becomes an Apple Reminders entry, which syncs to iCloud, which appears on my phone. If the bridge is down when this fires, the Iris reminder still saves. Bridge failures are silent warnings, not errors.
The bridge as a module
The initial version was one large server file. Fine to build but messy to maintain. It got split into focused modules: config, helpers, notes, reminders, files, shell, notifications, context. The main server file is now just routing and auth. Each domain lives in its own file.
The context endpoint is worth calling out specifically. When local machine tools load, the agent calls it to get the home directory, username, hostname, and which CLI tools are installed on the machine. That gets injected into the system prompt. So the agent knows where to look before it starts looking, and knows whether to reach for ripgrep or fall back to grep, without having to discover it mid-conversation.
Starting it automatically
The bridge needs to start when development starts. Adding it to the composer dev concurrently process was straightforward. The more interesting part was the restart story.
Node.js 18 shipped with a built-in --watch flag. It monitors the entry file and all its imports, and restarts the process automatically when any of them change. No nodemon, no extra tooling, no npm dependency. So changing a file under the bridge directory restarts the bridge without any manual action.
For the iris script that manages Docker operations, the bridge is now part of every lifecycle command. iris up starts the containers and starts the bridge. iris down stops the bridge and then stops the containers. iris deploy rebuilds and restarts the bridge, picking up any file changes that came in with the pull. iris bridge gives manual control when needed.
The bridge runs as a background process via nohup so it survives terminal close. Logs go to storage/logs/bridge.log.
The port conflict
One issue: if the bridge is already running via iris up and you then run composer dev, both try to bind port SET_PORT. The second one fails.
The first attempt at fixing this used a port check that would print a message and exit the concurrently slot if the port was already in use. That made things worse. Concurrently with --kill-others treats any process exit as a signal to kill everything. So the port check would exit cleanly, concurrently would kill the PHP server, the queue worker, and Vite. Not useful.
The fix was to switch from --kill-others to --kill-others-on-fail. That only propagates kills when a process exits with a non-zero code. The bridge failing to start because the port is taken exits non-zero, which does kill everything. That is still the right behaviour - a bridge failure should surface clearly. But if you intentionally stopped the bridge and are running it separately, you would not hit this case in normal usage.
The real answer is that Docker mode and Herd mode are not meant to run simultaneously. Pick one.
macOS permissions
The one thing that cannot be automated is the first-time permission grant for Apple Notes and Reminders. macOS requires explicit user approval before any process can control those applications via Automation.
If the bridge is running in the background when you first try to access Notes, the permission dialog may never appear. The call just fails silently. The fix is to run the permission-triggering command once from a visible terminal session, grant it, and then background processes inherit that permission going forward.
What it feels like
The file and shell tools worked on the first try. Git branch, file reads, directory listings, ripgrep searches: instant, correct, no ceremony.
Apple Notes returned an error on first run, which was the osascript permission issue described above. After granting it, worked fine.
The notification tool had a small bug where accessing an optional parameter without a null-safe check threw an exception that got swallowed into a misleading error message. Fixed.
The search result for a codebase query came back with the right file and line reference. That is the one that changes daily usage most. "Where is IS_DOCKER_PRODUCTION used?" used to mean switching to a terminal. Now it does not.
What is next
The bridge has git and gh in its allowlist already. The natural next step is dedicated coding tools that understand the git workflow properly: create a branch, make changes in a branch or worktree, open a PR, without needing the GitHub web UI or a terminal. A local coding session, not a remote one. That is coming.