PreToolUse hook that checks every tool call before the call runs.
Where the hook sits is the point. Permission rules live next to general-purpose interpreters, and a deny rule next to a wildcard Python grant stops an honest agent but not a redirected one. The guard reads each command before it runs and blocks the ones that name a protected path.
Install
In the app, open Enforcement, click Install / update guard, then click Register as PreToolUse. Or from a shell:hooks.PreToolUse. Your existing hooks keep their place and order. Before the change, settings.json is copied to settings.json.mbird-<timestamp>.bak.
What it blocks
Four layers, each switchable on its own.
The last two exist because each was a way straight past the first two. An agent that can edit the hook can switch the guard off in one write, and a connector call is not a command at all, so the command checks never saw it.
The guard blocks a call by exiting with code 2. Every call is recorded in
~/agent/audit/tools.jsonl.
The log keeps metadata — time, tool, session, working directory, and the size of any content — never the content itself. Earlier versions wrote the whole payload, including the full text of every file written. If you are upgrading,
--prune-log rewrites those old entries into the same shape; --prune-log --drop removes them instead.A match found only in content
If a protected path appears only inside the text of a file being written — not in the command, not in a path it names — the prompt says so and offers Allow once but not Allow always. A false alarm of that kind should not be able to open a standing exception.State
The guard reads~/.config/mockingbird/enforcement.json again on every call. Pausing and stopping take effect right away, with no restart, re-registration or reinstall.
When enforcement is paused or stopped, a banner appears on every screen and the menu bar icon becomes hollow.
What it checks
The guard reads what a call does, not the data it carries.
Otherwise, the guard doesn’t check file contents. A document that mentions a path isn’t accessing it, and checking prose added nothing. Text about a destructive command deletes nothing. A script that contains one is still refused: once when it’s written to an executable path, and again when it runs.
Unrecognized payloads
Unrecognized payloads are handled on the safe side:- Valid JSON, but no recognizable fields. The guard still checks every byte for a T0 path, because a T0 path in any argument means something is pointing at an original. It only checks for destructive commands when the tool name suggests it runs things:
bash,shell,exec,run,command,terminal,script,osascript,processoreval. That way a search query or message body can still mention those commands. - Not JSON at all. The guard checks the whole payload for both patterns.
How it behaves
These calls were tested against a sandboxedHOME, and every case passed:
Four rules re-run cases from this table on every scan: the two blocks, the prose that’s allowed and the script that’s refused. They set
MB_SELFTEST=1 so the guard responds as if it’s active, whatever the current mode.
Remove the guard
Click Remove in the Enforcement tab, or run_guard-uninstall.sh. This unregisters the hook and renames the script to t0-guard.removed-<timestamp> instead of deleting it. The doctrine’s N6, never delegate deletion, applies to Mockingbyrd too.