Block rules auto-block requests that match a method + host/path pattern before they reach the upstream server — no manual intervention. Where Breakpoints pause a request so you can decide each time, and Pauses let a host bypass the proxy entirely, a block rule fires automatically on every match and either returns a synthetic response or kills the connection outright.
A typical use: stop an app from ever reaching one endpoint. For example, block GET api.doordash.com/v3/dashes/*/active_assignments so the app behaves as if that call always fails — without you sitting on a breakpoint aborting it by hand.
How it differs from Pauses and Breakpoints
All three decide the fate of a matching request, but at different moments and with different intent:
| Feature | When it acts | What it does | Scope |
|---|---|---|---|
| Pauses | Before MITM | Skips the proxy entirely — passthrough, no inspection or logging | Host-level, proxy-server (PAC) only |
| Breakpoints | On match, mid-flight | Holds the request and waits for you to Continue / Edit / Abort | Host + path |
| Block rules | On match, before upstream | Automatically answers every match — Block (synthetic error), Drop (kill connection), or Mock (canned success response) | Host + path |
Block rules are the "set it and forget it" cousin of an aborted breakpoint: instead of pausing and waiting for you to hit Abort each time, the rule does it for you on every matching request.
Where block rules live
Rules are stored in the settings JSON data, at two scopes that combine:
| Scope | Location |
|---|---|
| Global | settings_global.data.blockRules — applies to every device |
| Per-device | settings_device.data.blockRules — applies to one device only |
The effective rule set for a device is global rules plus that device's rules (global ++ device). Like every other remote setting, changes fan out over Realtime — devices and the proxy pick them up without a reconnect.
A rule's fields
| Field | Meaning |
|---|---|
enabled | Toggle the rule on or off without deleting it. |
method | Optional HTTP method (GET, POST, …). Omit it to match any method. |
pattern | A host + path wildcard, using the same syntax as breakpoint patterns — e.g. api.doordash.com/v3/dashes/*/active_assignments, where * covers the dynamic segment. |
action | Block, Drop, Mock, or Script — see below. |
maxRuns | Optional run-count cap — the rule auto-disables after it fires this many times. Leave it empty for unlimited (the default — fire on every match); set 1 to fire once then stop. See Run-count cap & auto-disable. |
Block vs Drop vs Mock vs Script
- Block — return a synthetic error response. The status defaults to
403, with an optionalbodyandcontentType. The client gets a clean, immediate answer that never touched the upstream. - Drop — tear down the connection. The client sees a network failure, as if the request never connected — useful when you want the app to behave like the endpoint is unreachable rather than forbidden.
- Mock — return a custom response that looks like a real server reply. Set the status (defaults to
200), any headers (e.g.Content-Type: application/json), and a body. Where Block is meant to fail a call, Mock is meant to satisfy it with canned data — so the app proceeds as if the server answered normally. - Script — run real JavaScript (
onRequest/onResponse) on the matching flow instead of returning a fixed response. The rule borrows its own method + pattern; the code can mutate the request, synthesize a computed response, or conditionally block. This is the inline form of a Script and is gated by the admin-tierscriptscapability, stricter than the other three actions.
Run-count cap & auto-disable
By default a block rule fires on every matching request — that's the "set it and forget it" behaviour above. But some rules are meant to act exactly once. The classic case: a Mock rule that returns a synthetic 401 to make an app think its token expired, so it triggers a refresh. You want that to happen one time — if it answers every request with a 401, the app refreshes its token, retries, gets another 401, refreshes again, and spins in an infinite refresh loop until you disable the rule by hand.
The maxRuns field solves this. It's a cap on how many times a rule may fire; once the rule reaches the cap, it auto-disables itself and stops matching. Set it on any action type (Block, Drop, Mock, or Script):
maxRuns value | Behaviour |
|---|---|
| empty (default) | Unlimited — fire on every match, the same as before this feature existed. |
1 | Fire once, then auto-disable. The token-refresh trick. |
N | Fire N times, then auto-disable. |
In the dashboard editor a rule that has spent its budget shows an "auto-disabled" badge — distinct from a rule you switched off yourself — so you can tell at a glance why it stopped firing and re-enable or bump the cap if you want it to run again.
The count is per-device and survives reconnect
The run count is tracked per device and is persisted — it survives a device reconnect, a proxy restart, and an app relaunch. A maxRuns: 1 rule fires once ever on a given device, not once per session. (Internally each enforcer counts runs against the device, writes the count back through Supabase, and the auto-disable fans out over Realtime so the dashboard reflects it live.)
Global rules cap per-device, not fleet-wide
The cap is scoped to the device that hit it, and this matters most for global rules:
- A per-device rule that reaches its cap flips its own
enabledtofalsefor that device. - A global rule that reaches its cap on one device is suppressed only on that device — it keeps firing on every other device in the fleet. Capping a global rule on your phone never silently kills it everywhere.
So a fleet-wide maxRuns: 1 global rule fires exactly once on each device independently — every device gets its one shot.
Setting the cap
maxRuns is settable at creation, the same two ways as any other field:
- Dashboard — the "Max runs" input in the block-rule editor (leave it blank for unlimited).
- MCP / BusyBro — pass
maxRunson the rule object toset_block_rules_global/set_block_rules_device, or just ask BusyBro for "a one-time mock 401 on …".
Setting block rules
Set them in either of two ways:
- Dashboard — open the Blocks page from the settings nav, add a rule (method, pattern, action), and save. Per-device rules live under Devices → ‹device› → Blocks; global rules are the fleet-wide set that every device inherits.
- MCP / BusyBro — the
get_block_rules/set_block_rules_global/set_block_rules_deviceMCP tools read or replace the rule list, and you can ask BusyBro to add one in plain language (e.g. "block GET on api.doordash.com active assignments").
A blocked or mocked request still appears in YOUR capture feed, marked accordingly — so you can see exactly what was stopped or faked and confirm the rule is doing what you expect.
Undetectable to the app
Synthetic responses carry no internal or identifying headers — there is no x-busymate-* (or any other tell-tale) on a Block, Drop, or Mock response. To the client, a blocked 403 or a mocked 200 looks like an ordinary reply straight from the origin server, so the interception cannot be detected by inspecting the response.
The visibility is one-directional: the stopped/faked request shows up flagged in your capture feed, but the app on the device sees only a plain server response.
Coverage and the MITM limitation
Block rules are enforced in two places, covering every connection mode:
- The proxy-server — browsers, Android, and iOS in PAC mode.
- The iOS VPN tunnel — iOS in VPN mode.
Limitation: Path-level matching needs the host to be MITM'd / decrypted — the enforcer can only read the path on a host whose HTTPS is being intercepted. For a host that passes through as a raw, non-decrypted tunnel, only host-level matching is possible (the path is invisible). Add the host to your SSL-proxy / decryption list (proxy · iOS) first if you need path precision.
Out of scope
Block rules are deliberately simple. There is no regex (only the breakpoint-style host/path wildcards), no dynamic response templating — a Mock body is a fixed string, not computed per-request — and no scheduling: a rule is on or off.
Need a computed response? That's exactly what Scripts are for — JavaScript that mutates a request or response per-request, mocks a body your code builds, or rewrites a field of the real reply. Scripting is the advanced evolution of the Mock action, and a script can also ride directly on a block rule as its 4th Script action.
Example: block DoorDash active-assignments
Block the app from ever fetching active assignments:
| Field | Value |
|---|---|
enabled | true |
method | GET |
pattern | api.doordash.com/v3/dashes/*/active_assignments |
action | Block (status 403) |
{
"enabled": true,
"method": "GET",
"pattern": "api.doordash.com/v3/dashes/*/active_assignments",
"action": { "type": "block", "status": 403 }
}Because api.doordash.com is decrypted, the /v3/dashes/*/active_assignments path matches exactly — the * absorbs the dynamic dash id. Every matching GET now returns a synthetic 403 and never reaches DoorDash, while still showing up in the feed flagged as blocked. Switch the action to Drop instead if you'd rather the app see a connection failure than a 403.
Example: mock an empty assignments response
Instead of failing the call, return a valid empty result so the app renders cleanly as "no active assignments" rather than hitting an error path:
| Field | Value |
|---|---|
enabled | true |
method | GET |
pattern | api.doordash.com/v3/dashes/*/active_assignments |
action | Mock (status 200) |
{
"enabled": true,
"method": "GET",
"pattern": "api.doordash.com/v3/dashes/*/active_assignments",
"action": {
"type": "mock",
"status": 200,
"headers": { "Content-Type": "application/json" },
"body": "{\"assignments\": []}"
}
}Every matching GET now returns a 200 with Content-Type: application/json and the body {"assignments": []} — a response indistinguishable from one DoorDash itself could have sent. The app parses it as a normal, empty assignments list and carries on, while the upstream is never contacted. As with Block, no x-busymate-* header is attached, so the mock is invisible to the app.
Example: a one-shot 401 to force a token refresh
Return a synthetic 401 exactly once so the app thinks its token expired and refreshes — without looping. The only addition over a normal mock is maxRuns: 1:
{
"id": "44444444-4444-4444-4444-444444444444",
"enabled": true,
"method": "GET",
"pattern": "api.example.com/v1/me",
"action": {
"type": "mock",
"status": 401,
"headers": { "Content-Type": "application/json" },
"body": "{\"error\": \"token_expired\"}"
},
"maxRuns": 1
}The first matching GET gets the synthetic 401; the app reacts by refreshing its token. On that one firing the rule auto-disables itself for the device, so the next request — now carrying the fresh token — sails through to the real server and gets a real 200. No infinite refresh loop. The rule shows an "auto-disabled" badge in the editor afterwards; clear maxRuns or re-enable it to arm it again.
Next
- Scripts — go beyond a fixed Mock: write JavaScript that mutates requests and responses per-request.
- Breakpoints & resend — pause and edit a matching request instead of auto-blocking it.
- Remote settings & env — Pauses, decryption lists, and the rest of the per-device settings.