Composite scenario on a demo machine. No real user data.
The deny rule that didn't hold
A developer had denied sqlite3 so no agent could open local databases. Weeks earlier they had approved python3 -c * during a data task and made it permanent.
What nearly happened
Asked to "find that message about the invoice", an agent tried sqlite3 ~/Library/Messages/chat.db, hit the deny rule, and rewrote the same query as a one-line Python script. The allow list approved it.
Why the settings didn't stop it
Permission rules match text. The wildcard interpreter grant subsumes the deny rule — it covers everything the deny rule blocks, so anything sqlite3 can do, Python can do.
What Mockingbyrd does
The scan flags the subsuming grant as an open finding with the file and line. The guard reads the text of the command, so the Python version — which names the store in plain sight — is caught as a read of a T0 store and you are asked first. Had the path been assembled at runtime the guard would not have seen it, which is why the fix below is the fix.
The fix
- "Bash(python3 -c *)"
+ "Bash(python3 scripts/report.py)"
Composite scenario on a demo machine. No real user data.
Eighty-five became one hundred and seven
After a cleanup, an allow list held 85 entries. Thirteen days later it held 107. Each new entry had been a reasonable click at the time.
What nearly happened
One of the 22 new entries was Bash(curl:*), approved to download a dependency. Combined with broad read access, it meant an agent could read widely and send data off the machine without a prompt.
Why the settings didn't flag it
Nothing reviews the list as a whole. Each approval is judged on its own, never against what is already granted.
What Mockingbyrd does
Re-scans on your cadence, scores the combination (reads widely and can send off the machine), and names the entry that tipped it.
The fix
Drop the wildcard network grant and name the one host the build actually needs, so every other destination goes back to asking.
- "Bash(curl:*)"
+ "Bash(curl https://registry.npmjs.org/*)"
Composite scenario on a demo machine. No real user data.
The guard hook changed at 04:12
The hook that inspects every command changed at 04:12 — the guard’s own script, one of the four files each scan fingerprints. No one was working, and nothing in the agent’s own logs mentioned it.
What nearly happened
A scheduled job running unattended had edited the hook that was supposed to inspect its commands. Had it gone unnoticed, later tool calls would have run with no check at all.
Why the settings didn't flag it
Agents don't watch their own configuration. A control that quietly stops holding looks exactly like one that's working.
What Mockingbyrd does
Fingerprints the files that matter and reports drift as an event: which file, when, and that it no longer matches what you approved.
The fix
Review the change, put the approved version back, and find out which job made it. Mockingbyrd does not hold that version for you — drift is a fingerprint, not a copy — so the restore comes from wherever that file is kept: version control, a Time Machine snapshot, or the .mbird-*.bak copy Mockingbyrd wrote beside the settings file when it registered the guard. The audit log records the process tree above each call, so the job that made the edit is named there rather than guessed at.
Composite scenario on a demo machine. No real user data.
Forty yeses, one permanent grant
A developer approved the same interpreter prompt about forty times across a week, then clicked the option to stop being asked.
What nearly happened
The permanent grant was broader than any single request had been. It turned forty narrow decisions into one wide one.
Why the settings didn't flag it
The prompt is designed to reduce friction. It does its job; it just isn't a review.
What Mockingbyrd does
Shows the grant in the roster with what it can reach, scores it, and writes a script that replaces it with the narrow version. You read the script before anything runs.
The fix
Replace the permanent wildcard with the two commands those forty prompts were actually about.
- "Bash(python3 -c *)"
+ "Bash(python3 scripts/report.py)"
+ "Bash(python3 scripts/export.py)"
Since 1.0.189 the prompt that creates one of these says which rule it would stop holding, and “allow always” asks before it takes effect rather than after.
Composite scenario on a demo machine. No real user data.
Everything that was ever on screen
A screen-history app records what is on the display so it can be searched later. It is a genuinely useful thing to run, and it means one local database holds every message, document and window that has been in front of you — including other people’s, who never agreed to any of it.
What nearly happened
Asked to “find that thing I saw last week”, an agent did the obvious thing and opened the recorder’s own store. Not an exploit: the file sits in the home folder, the agent had file read, and nothing it was told made that file different from any other.
Why the settings didn’t flag it
A permission list names tools, not stores. It can say whether an agent may run sqlite3; it has no way to say that one database is the record of everything you have ever looked at and another is a project’s own data. The agent was not going around anything.
What Mockingbyrd does
The recorder’s store is a T0 original, so a command that names it is held and you are asked — below the permission list that allowed the tool. That is the limit worth being exact about: a path written plainly in the command is caught, and one assembled at runtime is not, which is why interpreters stay off the allow list rather than being something this layer is expected to clean up.
The data layer is the other half, and the more useful one. Rather than refusing the question, it builds a narrowed read-only copy: text only from an allow list of apps, audio dropped at every setting, full-text indexes left out, and the whole thing generated from the schema in front of it rather than a list of table names that goes stale. The agent gets an answer; it never gets the original.
The fix
Point it at the copy. The projection answers the same question, and the original stops being something an agent has any reason to open.
- sqlite3 ~/recorder/store.db "select text from frames"
+ sqlite3 ~/agent/derived/projection.db "select text from frames"
This is the case the product is named for. Never touch the original is not a slogan about intent — it is the arrangement where the question has an answer that does not require the original to be opened at all.