Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ A click on a subagent in the sidebar opens its tool calls beside the agent. A re

Other agents are recognised by name, and their state is read from what they print. It can lag, be wrong, or miss "waiting for you".

Each agent is described by a manifest, `plugins/<name>/plugin.json` in the source. To add another agent without a new nerd, put its manifest in `~/Library/Application Support/nerd/plugins/<name>/plugin.json` and choose **Reload plugins and agents** in the main menu. Such a manifest adds its hook to a JSON config under `~/.<name>` or `~/.config/<name>` and never replaces a file. A manifest that breaks a rule, or names a built-in agent, is skipped, and the log says why.
Each agent is described by a manifest, `plugins/<name>/plugin.json` in the source. To add another agent without a new nerd, put its manifest in `~/Library/Application Support/nerd/plugins/<name>/plugin.json` and choose **Reload plugins and agents** in the main menu, in a nerd window opened from your terminal. Such a manifest adds its hook to a JSON config under `~/.<name>` or `~/.config/<name>` and never replaces a file. A manifest that breaks a rule, or names a built-in agent, is skipped, and the log says why.

## Requirements

Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ Out of scope:
- **A plugin reaches only its network tier.** Its own Seatbelt profile allows nothing, or `localhost` only. With hosts and ports it reaches only the port of a proxy nerd runs for it alone, and the proxy connects only to those hosts on those ports: a name only at its public internet addresses, never this Mac or a network it is on; an IP address as the manifest writes it. A plugin never opens a terminal device, and does not read the credential folders and `~/Library` listed in docs/plugins.md; it can read the command line and environment of the user's other programs, which macOS does not let a sandbox hide.
- **The broker runs what the server asks.** It starts panes and plugins outside the sandbox from the server's requests, so the server's sandbox is a guarantee about nerd's own code (no network, no telemetry), not a wall around a compromised server.
- **A nerd reached over ssh is trusted like a local one.** What the remote server sends reaches your terminal as it would from a local server.
- **Panes are outside it.** A helper started before the sandbox launches every pane. A pane left at Everything and Open keeps your full access to files and the network; its isolation (**Isolation…**) narrows that with a Seatbelt profile of its own, a proxy for Only these providers, or another account. An agent's own Seatbelt sandbox cannot run inside an isolated pane.
- **`open` is outside it.** Opening a file or link "the way the system would", and opening a new terminal window, go through LaunchServices, which starts those programs outside the sandbox.
- **Panes are outside it.** A helper started before the sandbox launches every pane. A pane left at Everything and Open keeps your full access to files and the network; its isolation (**Isolation…**) narrows that with a Seatbelt profile of its own, a proxy for Only these providers, or another account. An agent's own Seatbelt sandbox cannot run inside a pane with This folder only, Only these providers or None.
- **A pane with a Seatbelt profile (every isolated pane but Another account with Network Open) cannot ask macOS to start programs.** Its profile refuses LaunchServices (`open`), new launchd jobs (`launchctl`), AppleEvents to other apps (`osascript -e 'tell application …'`), saving macOS preferences (`defaults write`), and lookups of the services that run Automator workflows, shortcuts and user scripts (checked by hand for user scripts; Automator workflows and shortcuts are not yet). It does not cover a program that talks to a system service directly over XPC, launchers nerd does not know of, files a pane with Files Everything writes that run later outside it (`~/Library/LaunchAgents`, `~/.zshrc`), or a pane started before an update until its program restarts. A pane set to Another account with Network Open has no Seatbelt profile. The way past is a profile that denies by default and allows only the Mach services agents need.
- **`open` is outside it.** Opening a file or link "the way the system would", and opening a new terminal window, go through LaunchServices, which starts those programs outside the sandbox. nerd does not hand a file that runs a program to its app. It refuses app, workflow, playground and terminal types, Finder aliases, sockets and devices. It opens a file only in an app chosen at the click that has a bundle id, is not a terminal or script runner, and lives in the system, `/Applications`, `/Library` or `~/Applications` folders, with no folder holding it or its program in a folder another account may write (such as `/Library/Caches`), outside the clicked path and the clicked pane's folder; scripts, files with no extension and every other file open in the plain-text app chosen the same way (TextEdit when that app does not qualify). A package opens in such an app, another folder with an extension is shown in Finder, a plain folder opens in Finder, and a plain folder macOS would open with anything else is refused. A plain folder that a pane replaces with a program at the instant of the click is opened by what it became; an app that a pane with Files Everything installs in those folders counts like any other, as does one installed by a pane whose folder holds `/Applications` or `/Library` (`/` itself, or those folders or inside them) when the click comes from elsewhere.
- **A sandbox that fails to start does not stop the server.** The failure is written to `daemon.log`, shown by `nerd --list`, and shown in the window of every client that attaches.
- **A release is signed, and what that covers depends on the way in.** Its `.sha256` is signed with nerd's release key, kept off GitHub; install.sh carries the public halves of that key and its backup, and installs nothing whose signature does not check. An update from nerd's menu uses the install.sh built into the running nerd, so someone who can replace a release on GitHub cannot make it install theirs. `curl … | sh` downloads install.sh itself from nerdterm.dev, so it is only as safe as nerdterm/nerd's release tags and the site; it refuses a release older than the nerd already in its install folder (by default ~/.local/bin), but a first install takes whichever signed release GitHub calls the latest, older ones included. Homebrew pins the archive's sha256, written into the formula only after the release script checked the signature, so it is as safe as nerdterm/homebrew-tap. It cannot stop someone holding the key, a release held back or with its signature removed (nerd then reports the release has no signature yet and stays where it is), or an older nerd that never updates; the changelog shown before installing is not signed. Releases before 0.15.0 were not signed, and a nerd before 0.15.0 installs the next release without checking its signature; every update after that is checked. The installer refuses an unsigned archive given by hand unless `NERD_INSTALL_UNVERIFIED=1` is set, and a signed archive whose nerd does not report the version in the archive's name. The release key signs only the release commit on master: the release script refuses to build and sign any other commit, or a binary reporting another version. Building and testing run every dependency's build code as the maintainer, so the script reads its signing script, formula script and install.sh from the release commit in git before it builds and runs that text afterwards, and after the build runs only the system's own commands and the nerd it built until the key has been used: a build that rewrites those files or puts commands of its own on the maintainer's PATH cannot run them in their place. It cannot stop build code that keeps running as the maintainer from reading the terminal while the key's passphrase is typed, using the maintainer's GitHub login, changing the binary being built, or waiting for a later release; only building releases as a separate macOS user would.
- **Both ends check the other's user on every connection.** The server talks only to processes of its own user, and a client refuses a server socket another user created first, for example at a `NERD_SOCKET` in a shared directory, before it sends anything. Such a socket still keeps nerd from starting there: keep `NERD_SOCKET` in a directory only you can write. That check is the client's, at nerd's own socket; when the plugin socket beside it cannot be nerd's, the server starts without plugins and logs why.
10 changes: 7 additions & 3 deletions docs/isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,26 @@ A pane starts with your access: its program, an agent included, can read and wri
|---|---|
| Open | Your network, as you. |
| Only these providers | Only the hosts and ports that the agents you choose name in their manifests, through a proxy nerd runs for that pane alone. For pi, **Choose providers…** narrows that to some of its providers. The agent must honour `HTTPS_PROXY`; one that does not gets no network. Agents whose manifests name different ports cannot share a pane. |
| None | No internet and no name lookups. Services on this Mac (`localhost`) stay reachable. |
| None | No internet and no name lookups. Services on this Mac (`localhost`) stay reachable, `ssh localhost` too when Remote Login is on. |

The hosts in the agents' `plugin.json` are a starting point: check them against what your agent contacts. pi's names every provider pi ships with, except four you add yourself if you use them: Bedrock outside us-east-1 and eu-central-1, Vertex and Azure use addresses of your own account; Cloudflare (`api.cloudflare.com`, `gateway.ai.cloudflare.com`) can fetch any URL for an account token, so it would make the pane's network open. In a pane set to Only these providers, Bedrock works with keys, not with SSO or profile login. The proxy checks the name a program asks it to connect to, not the name inside the encrypted connection, so a service behind the same address as an allowed host (a shared CDN) is reached too.

**Choose providers…** appears under the chips once pi is chosen and lists pi's providers, each with the hosts it opens: `deepseek` opens `api.deepseek.com`, `openai` opens `api.openai.com` and `auth.openai.com`, `github-copilot` opens GitHub's Copilot hosts and `github.com`, which pi also downloads its tools from. **all** is every provider, those added in a later version of pi included; a list of providers stays that list, so a provider added later stays closed. The pane is saved as `pi/deepseek`, one entry per provider; a pane saved before this choice existed is saved as `pi` and keeps every provider. A provider that a later nerd renames or removes stops the pane rather than opening more: it does not start, and the notice names the pane and the provider; the **Isolation** window shows it as no longer in pi's manifest and drops it on save. Only built-in agents name providers; a plugin's or your own agent manifest names its hosts as one list.

A pane's `~/.pi/agent/*.lock` is writable from every This-folder-only pane: one session can hold another pi session's lock, which only delays that session until the lock is deleted.

**This folder only** also keeps one session from planting code or instructions for another where agents and tools look for them. The temp folders (`/private/tmp`, `/private/var/folders`) stay writable and are shared by every session and tool: what one session leaves there, another can read. Inside the places it may write, these stay read-only: shell, git and editor startup files, `~/.local/bin` and other folders ahead on `PATH`, agents' settings and memory, Claude Code's task list, Codex's shared databases, Qwen Code's workflow scripts, what agents source or run later from their own folders, and the known caches whose contents run later (pnpm, uv, deno, Go, Playwright, Poetry, Yarn, node-gyp, pip, Cypress, SwiftPM, Copilot, the system Python's bytecode, Homebrew, ccache, sccache, esbuild, Bazelisk, Coursier); another cache under `~/Library/Caches` stays writable. Seatbelt sees a folder moved in whole as one being made, so a missing `~/.<agent>/projects` folder, or a project folder in it an agent has not made yet, can still be put in place with memory or Qwen Code's workflow scripts inside. When the pane's folder is your home folder, its dot files, `~/Library` and `~/bin` stay read-only too, except `~/Library/Caches` and the agents' session folders; choose a project folder for a real limit. The **Isolation** window lists what stops working.
**This folder only** also keeps one session from planting code or instructions for another where agents and tools look for them. The temp folders (`/private/tmp`, `/private/var/folders`) stay writable and are shared by every session and tool: what one session leaves there, another can read. Inside the places it may write, these stay read-only: shell, git and editor startup files, `~/.local/bin` and other folders ahead on `PATH`, agents' settings and memory, Claude Code's task list, Codex's shared databases, Qwen Code's workflow scripts, what agents source or run later from their own folders, apps in `~/Applications`, and the known caches whose contents run later (pnpm, uv, deno, Go, Playwright, Poetry, Yarn, node-gyp, pip, Cypress, SwiftPM, Copilot, the system Python's bytecode, Homebrew, ccache, sccache, esbuild, Bazelisk, Coursier); another cache under `~/Library/Caches` stays writable. Seatbelt sees a folder moved in whole as one being made, so a missing `~/.<agent>/projects` folder, or a project folder in it an agent has not made yet, can still be put in place with memory or Qwen Code's workflow scripts inside. When the pane's folder is your home folder, its dot files, `~/Library` and `~/bin` stay read-only too, except `~/Library/Caches` and the agents' session folders; choose a project folder for a real limit. The **Isolation** window lists what stops working.

A pane that can write its folder can write the repository's `.git` too. nerd's own git there (the file tree's marks, diffs and review, **New worktree…** and **Delete worktree checkout…**) runs none of the repository's own hooks, filters, fsmonitor or lazy fetch; your global git config, such as Git LFS, still applies. A worktree made from the menu is therefore checked out without the repository's post-checkout hook or its local filters, and the notice names the filters left out (Git LFS installed with `--local`, for example). nerd refuses to make or delete a worktree from the menu when the repository's own config includes other files (`include.path`, `includeIf`), since what they make git run in another checkout cannot be checked first. Your own `git` in that folder runs all of it as usual: a hook a pane planted runs at your next `git checkout` or `git commit` there. nerd reads the repository's config just before its git does, so a pane that swaps the repository's config files in between can still, at the right moment, have a filter of its own run. A git older than 2.31 cannot be told to leave the filters off: with it, nerd shows no marks, diffs or review, refuses **New worktree…** and **Delete worktree checkout…**, and `daemon.log` says why.

Local sockets: with Network **Open**, a This-folder-only pane reaches no ssh agent and no Docker, colima, OrbStack, Rancher Desktop, lima or podman socket, so `git` over ssh and `docker` do not work there. With **Only these providers** or **None**, a pane reaches no Unix socket but nerd's message socket.
Local sockets: with Network **Open**, a This-folder-only pane reaches no ssh agent and no Docker, colima, OrbStack, Rancher Desktop, lima or podman socket, so `git` over ssh and `docker` do not work there, and it cannot reach the sockets nerd knows of for tmux, screen, WezTerm, Zellij, iTerm2 and kitty outside it: tmux's and Zellij's own folders, screen's, WezTerm's and iTerm2's, and kitty's where `KITTY_LISTEN_ON` names a file. Whatever its network, a This-folder-only pane cannot write in those places either, so it cannot leave a socket there for them to trust. With **Only these providers** or **None**, a pane reaches no Unix socket but nerd's message socket. With Network **Open**, tmux and Zellij inside the pane work and keep sessions of that pane's running program, which tmux outside nerd and other panes neither see nor change; after the program restarts or the pane closes, detached sessions keep running but cannot be reached from a pane. macOS's own `screen` starts no session under any Seatbelt profile, so it does not run in a pane with This folder only, Only these providers or None. A `TMUX_TMPDIR`, `ZELLIJ_SOCKET_DIR` or `SCREENDIR` set in a shell startup file replaces nerd's: tmux there then fails, or reaches the outer session when nerd's server was started without that value. Still reachable with Network Open: kitty listening on `tcp:` and kitty instances other than the one in `KITTY_LISTEN_ON`, a tmux started with `-S <path>` unless nerd's server was started inside it, iTerm2's websocket on `localhost`, and the servers of VS Code, Emacs and Neovim; a pane with Files Everything can write a This-folder-only pane's tmux folder, as it can that pane's own folder.

A pane with This folder only, Only these providers or None cannot ask macOS to start a program for it: `open`, `osascript -e 'tell application …'`, `launchctl` and `defaults write` are refused, and so are lookups of the services that run Automator workflows, shortcuts and user scripts (checked by hand for user scripts; Automator workflows and shortcuts are not yet), so links and files do not open from there, AppleScript cannot control other apps, new launchd jobs are refused, and programs cannot save macOS preferences (`defaults`, Xcode and other Apple tools). A `terminal-notifier` hook loses what its notification opens. Ctrl+click in nerd still opens links. This does not cover a program that talks to a system service directly over XPC, launchers nerd does not know of, `launchctl kickstart`, which is not yet checked, or a pane that started before an update, until its program restarts.

Programs that need the setuid bit (`ps`, `top`, `sudo`) do not run in a pane with This folder only, Only these providers or None: Seatbelt refuses them under a profile. An agent's own Seatbelt sandbox cannot run there either; the **Isolation** window says how to turn it off for each agent.

A pane set to Another account with Network Open has no Seatbelt profile: setuid programs and an agent's own sandbox work there, and the refusals above do not apply. Network None or Only these providers closes that.

**Another account.** **Settings… → Messages → Accounts…** shows the commands that create the account; nerd never runs them. Its panes ask for your password (sudo) each time they start, and run a copy of nerd that only root can change. Put it in place once, and again after each nerd update:

```sh
Expand Down
Loading
Loading