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 SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ Out of scope:
- **`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.
- **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.
- **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.
6 changes: 4 additions & 2 deletions docs/isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ A pane starts with your access: its program, an agent included, can read and wri
| 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. |

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 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. 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, 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.

Expand Down
4 changes: 2 additions & 2 deletions docs/messages-between-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Agents in different panes can write to each other, and to you, through nerd. Eve
- **A name is the opt-in.** A pane named with **Rename pane…** (in a space of one pane, also on the space's row) can receive. A name is 1 to 32 letters, digits, `.`, `_` or `-`, does not start with `-`, is not all digits, and is not `you` or `all`. An unnamed pane neither sends nor receives.
- **Sending.** From a pane: `nerd send <name|all|you> <text>` (`-` reads the text from standard input), with `--reply <id>` to answer, `--ask-route <name>` to ask you for a route, and `--wait <secs>` to wait for the answer. `nerd agents` names the calling pane, then lists the panes it may write to (`you` is the person at nerd's window), `nerd inbox [--all] [--show <id>]` reads its mail, and `nerd wait <id> [--thread] [--count N] [--timeout S]` waits for replies. Exit codes: 0 sent, 3 held for your answer, 1 refused, 2 timed out. A send refused because no pane has that name or a route forbids it ends with whom the pane may write to. When that list changes after the pane last saw it (with `nerd agents` or such a refusal: a name, a space or tab, a route, an account), its next `nerd send`, `nerd inbox` or `nerd wait` says so once on standard error; nerd never types or injects this into the agent's session.
- **The frame.** Every delivered message starts with `[message from <name> via nerd, id <id>; reply: nerd send --reply <id> "…"]`. The sender is read from the kernel, never from the text; a line of the text that starts the same way gets `> ` in front.
- **Routes.** Mode **Open** lets panes of your account write to each other and asks you about other accounts; **Strict** allows only what a route allows. A route says Allow, Ask or No from one side to another: a pane, a space, a tab, an account, an agent, `you` or everyone. The most specific route wins, and a tie goes to the stricter. Writing to `you` is allowed unless a route to `you` says Ask or No. A reply to a message passes unless a route says No; a refused or held message opens no way back. Answering a held message can allow that pair for an hour.
- **Routes.** Mode **Open** lets panes of your account write to each other and asks you about other accounts; **Strict** allows only what a route allows. A route says Allow, Ask or No from one side to another: a pane, a space, a tab, an account, an agent, `you` or everyone. An agent is the one nerd detects from the program in front, by its name and arguments: a route for an agent is a label, which any program in a pane can wear. The most specific route wins, and a tie goes to the stricter. Writing to `you` is allowed unless a route to `you` says Ask or No. A reply to a message passes unless a route says No; a refused or held message opens no way back. Answering a held message can allow that pair for an hour.
- **Where.** **Messages…** in the main menu shows the mode, a matrix of which space may write to which (a click on a cell opens the panes of those two spaces, where a cell cycles that pair's route), what waits for you, and today's threads. **Set up a topology…** writes routes from a preset. **Routes map…** shows every named pane and its routes on one screen. **Settings… → Messages → Accounts…** admits other accounts.
- **A held message.** A click on it in the sidebar's needs-you list, on a `held N` cell, or on its row under what waits for you opens it: who wrote to whom, why nerd holds it in a plain sentence, how long it has waited, its text, and its buttons (deliver, deliver and allow 1 hour, drop, reply; for a route request allow, ask, no). Once it is delivered, dropped, decided or, for a message to you, answered, the next held message between the same two panes follows; when none is left the view goes back to where it was opened from.

Expand Down Expand Up @@ -46,7 +46,7 @@ Above 20 KiB only the first 2 KiB is delivered, with `nerd inbox --show <id>` fo
| Cursor | Its hook: `followup_message` at stop. The CLI may not fire it; then typing when idle |
| pi, OpenCode | nerd's extension |

An agent with no better path gets the message typed in, only while it is idle and is the program in front of its terminal. Nothing is ever typed into a pane waiting for your answer. `via` in the Messages window says which path was used.
An agent with no better path gets the message typed in, only while it is idle and is the program in front of its terminal. Nothing is ever typed into a pane waiting for your answer. A question that comes up after the message was typed holds its Enter for as long as it shows; once it is gone, Enter goes in only if the agent is idle and in front and nobody typed into the pane since. Otherwise, or when nerd was updated in place in between, the message stays typed in the agent's input, not sent: it counts as delivered, with the note "typed, not submitted" in `messages.jsonl` and a line in `daemon.log`, and is never typed a second time. After nerd stopped or crashed in between, the pane and what was typed into it are gone, and the message still counts as delivered. `via` in the Messages window says which path was used.

**When an agent's own sandbox blocks `nerd send`.** `nerd send` talks to the socket named by `NERD_MSOCK`. Claude Code with `/sandbox`: add `"sandbox": {"network": {"allowUnixSockets": ["<the path in $NERD_MSOCK>"]}}` to its settings (`echo $NERD_MSOCK` in the pane prints it; each server has its own). Codex with `workspace-write`: `[sandbox_workspace_write] network_access = true` and a `unix_sockets` allow for the socket path. Gemini's permissive profile needs nothing. pi and OpenCode have no OS sandbox. For any other agent, test it. nerd never edits these settings. nerd knows a sender by its processes, not by the pane's variables, so an agent whose tools run outside the pane cannot send: Codex with `--remote` (it still receives, by typing), or a Claude Code session behind `claude attach`, whose session runs in Claude Code's background host. When such an agent carries a pane's variables, the refusal names that pane; start the agent in the pane itself.

Expand Down
3 changes: 2 additions & 1 deletion docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ it, and takes nerd's hooks out of its agent's config; `--plugin-on` brings both
window's to do there.

**The catalog.** `nerd --plugin-catalog`, or **Refresh catalog**, fetches the catalog from
`https://github.com/nerdterm/nerd-plugins` into the state root's `catalog/`; nothing else ever fetches it. Each plugin
`https://github.com/nerdterm/nerd-plugins` into the state root's `catalog/`; nothing else ever fetches it. The catalog
is not signed: it is as trustworthy as that repository and HTTPS to GitHub. Each plugin
is marked **reviewed** (the maintainer approved that release), **checked** (risk level 0, approved by the catalog's
checks alone), **not in catalog** (another address or another commit than the catalog's), or **by hand**. A catalog
install takes exactly the commit the catalog reviewed, refuses when its tag no longer points there, and recomputes the
Expand Down
Loading
Loading