Skip to main content
The roster answers one question: who can reach what. Rows are the things that can act — the agents themselves, the profiles and subagent definitions they run under, scheduled jobs, connectors, and installed applications that declare reach into the same stores — including the ones that turn out to be assistants in their own right. Columns are the capabilities the doctrine cares about: files, shell, code runners, network, delete, secrets, originals and connectors.
The roster reads what is already on your Mac. It sends nothing anywhere, and it changes nothing it reads.
Rows are named for the tool, not the activity, because the first thing you want to know is what a row is. Each row carries its model where one is declared, plus a one-line summary — a subagent’s comes from its frontmatter, a connector’s from what it reaches.

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.
Nerd Mode toggles between the two. Intuitive mode shows the map and keeps cards only for agents that carry a finding. Both read the same rows, and clicking any card opens the evidence behind each cell.
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.
Dashes travel from the hub outwards along each wire, in the direction the data goes. The motion carries a fact rather than decorating: a wire to an agent that can send data off your Mac is drawn brighter and thicker than one that only reads. If you have Reduce Motion turned on, the wires are drawn static. Past four agents, cards shrink to three capability lines and a “+n more”. Cards sit three to a row, and the hub holds a cell of its own in the middle of the grid rather than floating in a gap beside it. Sixteen are wired, chosen by how far each row reaches rather than by the order the roster happened to build them in; anything beyond that sits below with no wire drawn. A card can never grow into the row beneath it — it truncates instead.

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.
Entitlements and TCC disagree in both directions, which is exactly why both are worth reading. An app can declare an entitlement macOS never granted, and hold a grant its entitlements do not suggest. That is also why entitlements get their own state, declared, not confirmed, rather than counting as granted. An entitlement is a request, not a grant. Calling it granted would be a lie with a colour attached.

Reading TCC needs Full Disk Access

To report who else holds a broad read on your Mac, Mockingbyrd has to hold one itself.
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.
The list is usually more surprising than expected. Office and PDF software declares a great deal — files, network, contacts, calendars, keychain and Apple Events, which is the read-and-send pairing §3 N2 exists to prevent. But a long list of declarations is a sign of being sandboxed, not of reaching furthest. The apps that declare least are often the ones that reach most, for the reason two sections down.
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’s NS*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.
There is no list of vendor names anywhere in this. A name list is wrong the day after it is written and says nothing at all about an app nobody has heard of. The deciding sentence is quoted on the row, so a verdict you disagree with shows its reasoning rather than asserting itself.
Three things keep them from being mistaken for the agents you configured:
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.
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.
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.
An app that runs several helper processes from bundles nested inside its own — as Electron apps do — is one row, not six. The outermost .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
An absence is not always a smaller claim. On these rows a TCC-gated entitlement still means declared — what the app may ask macOS for, not what macOS allowed. The sandbox is the exception and runs the other way: not declaring it means more reach, not less. The capability tags show granted apart from declared so the difference is visible where it matters.

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.
Unguarded rows. Mockingbyrd’s guard is a Claude Code hook. An agent it cannot watch — Codex, for example — is described from its configuration, never from observation, and carries an unguarded mark. Without it, the agent nothing inspects shows fewer marks than the one that is inspected, and so reads as the safer of the two. It is not.

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

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.
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.
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.
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 around permissions.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.
The classification is checked against bundles the test builds and signs itself, rather than against whatever this Mac happens to have installed. A fixture needs a real Mach-O executable: a bundle whose executable is a shell script signs as “bundle with generic” and silently carries no entitlements at all. Headless, the roster prints to a shell:
To ask about a single bundle — including one that is not running — and see the sentence the verdict was read from:

Back to architecture