The roster reads what is already on your Mac. It sends nothing anywhere, and it changes nothing it reads.
Map and matrix
The matrix answers “who can do X” by scanning a column. It does not answer “what is this agent”, which is the question you ask first. So the default view is a map.Map
A card per agent around a hub. Each names its kind, its model, what it is for, and every capability it holds — in order of consequence, with originals and secrets first.
Matrix
The full grid. Use it to scan one capability down a column across every agent at once.
Every cell in the matrix carries a shape as well as a colour — granted
!, denied ✓, asks first a raised hand, declared *, not granted −, not visible ?. Granted and denied were red and green, which is the one pair that collapses for the commonest colour blindness, and they are the two states the grid exists to tell apart. Hovering a cell names its state.Where each cell comes from
A tool’s own config is what that tool chose to write about itself. The roster reads past it three ways.1
Connectors
MCP is the common denominator across tools, and each one keeps its list somewhere else. A single scan covers Claude desktop, Claude Code (including the per-project lists in
~/.claude.json), Cursor, Windsurf, Continue, Zed, VS Code and Codex. A server declared in two tools becomes one row that says so.2
Entitlements
An app’s code signature is readable with no permission at all, and it is vendor-independent. It tells you what the app may ask macOS for.
3
TCC
What macOS actually allowed. Mockingbyrd opens the TCC databases read-only and immutable, so no journal is written beside a system database.
Reading TCC needs Full Disk Access
This is a genuine trade, and the app states it in the interface rather than burying it. Mockingbyrd asks for nothing on its own. Without the grant it says so plainly, keeps every row at “declared, not confirmed”, and offers a button that opens the right settings pane. Granting it stays your act, because macOS does not let an app grant itself this. That is the point, not a limitation.Apps that are not Claude, Codex or Cursor
The roster used to recognise four process names — Claude, Codex, ChatGPT, Cursor — so an AI app called anything else was invisible however much it could reach. A tool running right now, declaring access to your contacts, calendars, photos and reminders, appeared nowhere. Every running app outside/System is read now, and one gets a row when its code signature
declares an original store or the keychain. The test is reach, not vendor: the question this
page answers is what can get at your things, and an app saying so in its own signature is as
good a source as a config file.
Sensors alone are not enough. Location and the camera are declared by half the Mac, and a row
for each would bury the rows that matter.
A TCC-gated entitlement on these rows is declared, never granted — the same state
entitlements get everywhere else here. What macOS actually allowed is in TCC, which needs Full
Disk Access to read. The sandbox is the one exception, and it runs the other way; see below.
Three kinds, read from the app’s own words
Filing every installed application under one word flattened a difference that matters. A video editor that may read your contacts to fill in a credit is not making the same statement as an assistant that reads all four original stores and says outright that it can send your mail. The verdict comes from each bundle’sNS*UsageDescription strings — the sentences Apple
requires a vendor to write and shows you verbatim in the permission dialogue. They are the
vendor’s own account of why they want a thing, and you have already been shown them once.
A host attributes the ask to something other than itself. An assistant is itself the actor.
Where a bundle claims both — a terminal that also has an assistant in it — host wins,
because anything I run may reach this is the larger of the two claims.
A host is worth its own kind because its reach is not really its own: it belongs to whatever
runs inside it. The T0 guard lives in one of these.
Three things keep them from being mistaken for the agents you configured:
Counted apart
Counted apart
The hub reads something like “3 agents · 8 connectors · 1 assistant · 2 hosts · 15 apps”,
with the numbers measured on your own Mac. An installed assistant is counted on its own rather than folded into either neighbour. It is not an agent
in the roster’s sense — nothing here configured it, and the guard cannot see it — but
counting it as “an app” beside a PDF reader was the thing this number got wrong.
On the map, or in the panel beneath it
On the map, or in the panel beneath it
Assistants and hosts are wired to the hub, because an app that reads every original store on
the machine is exactly what a picture of “what can reach this Mac” is for. Plain apps are not
drawn — a dozen or more cards with no wires would bury the ones that have them — so they
get their own panel below, sorted by reach, with the capability tags on every row. Everything
appears in the matrix either way, and in Intuitive mode as a card that names the assistants
first and separately.An earlier version counted apps in the hub and showed them nowhere, which made a working
feature look broken: the diagram said “17 apps” and offered no way to see which seventeen.
Unguarded, and said so
Unguarded, and said so
The T0 guard is a Claude Code hook. Nothing inspects what any of these apps does, and every
row says so. It matters most on an assistant row: an unguarded agent that reads your
originals and can send from them is the shape the whole doctrine is about.
.app is what counts.
Why the sandbox reads backwards
Entitlements are sandbox permits. That makes them exactly the wrong measure of reach for an app that has no sandbox.com.apple.security.network.client is how a sandboxed app asks for the network. An app
outside the sandbox needs no permit at all: it declares nothing and has unrestricted network,
plus every file your account can reach that TCC does not cover — $HOME/.ssh, $HOME/.config,
every repository on the disk.
Reading that absence as “no network” got the answer backwards. It meant the §3 N2 pairing —
reads an original store and can send — went unreported on the rows that had it, while being
reported faithfully for sandboxed apps that reach less.
The roster reads com.apple.security.app-sandbox first now. When it is missing, network and
files are recorded as granted rather than left blank, and the cell carries the reason:
no app-sandbox entitlement, so outbound network needs no permit and nothing gates it
Whose thing each row is
A row named Analyst tells you nothing on its own. Every row carries the tool it belongs to — “Claude Code profile”, “Codex session”, “Connector · Claude Code + Cursor” when two tools declare the same one — because the first question a roster answers is what a thing is. Agents and connectors are counted apart, too. Two agents with eight plug-ins is not ten peers.Declared versus running
The roster also looks at what is loaded, not only what is declared, because the gap between those two is where the interesting things live. A connector running with no local declaration at all — started by an app whose connector list lives elsewhere — is listed, flagged and marked RUNNING. Candidates have to look like a package name or they are discarded, since a command line carries tokens, paths and JSON that you would not want collected. The several spellings one server appears under (a launcher, a Python module path, a bare binary) collapse into one row.Rules that keep the cells honest
A deny narrows a capability; it does not remove one that is also granted
A deny narrows a capability; it does not remove one that is also granted
Reporting “shell: denied” because a single narrow deny entry exists would read as though the agent cannot run commands, when it plainly can. A deny list shapes a capability. It rarely removes one.
A connector is rarely one capability
A connector is rarely one capability
Most connectors worth worrying about do two things at once, and recording only one of them understates them. A messaging connector reads an original store and can send from it — that pairing is the combination §3 N2 exists to prevent, and it is invisible while the row says “network” alone. A cloud API connector is not holding one key inside the tool; it reads the credentials sitting on your Mac, which is a larger thing. A screen-control connector reads whatever is on screen and can click it, and no file permission covers either half.A connector whose name still says nothing is reported as unclassified rather than assumed harmless. That has not changed: the roster would rather say it does not know.
An entry naming a T0 path is about originals, however it is spelled
An entry naming a T0 path is about originals, however it is spelled
A read of the screen-history store is not a file permission. It is access to an original, and the roster files it that way regardless of which permission syntax expressed it.
A row nothing local can see reads 'not visible here', not blank
A row nothing local can see reads 'not visible here', not blank
Some apps configure their tools inside the app and on their maker’s servers, where no local check can see them. An empty row would say “reaches nothing”. The truth is the opposite, so those cells read as unknown and the notes explain what a row of question marks means.
Why a matrix at all
The grid exists to make combinations visible. Reading widely and being able to send is the doctrine’s N2 in a single row. An interpreter grant is N5. A scheduled job on a shared surface is N7. It also catches what a file deny list cannot. A connector that reaches a T0 store goes aroundpermissions.deny entirely, because it never touches the path.
Checking it
scripts/check-roster.py builds a home directory to order and asserts each cell and each finding — 57 checks.
