Block rules

Auto-block requests matching a method + host/path pattern before they reach upstream — return a synthetic response or drop the connection.

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:

FeatureWhen it actsWhat it doesScope
PausesBefore MITMSkips the proxy entirely — passthrough, no inspection or loggingHost-level, proxy-server (PAC) only
BreakpointsOn match, mid-flightHolds the request and waits for you to Continue / Edit / AbortHost + path
Block rulesOn match, before upstreamAutomatically 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.

requestblock rule matchBlock → 403 bodyDrop → connection killedMock → 200 + custom bodyupstream never reached

Where block rules live

Rules are stored in the settings JSON data, at two scopes that combine:

ScopeLocation
Globalsettings_global.data.blockRules — applies to every device
Per-devicesettings_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

FieldMeaning
enabledToggle the rule on or off without deleting it.
methodOptional HTTP method (GET, POST, …). Omit it to match any method.
patternA host + path wildcard, using the same syntax as breakpoint patterns — e.g. api.doordash.com/v3/dashes/*/active_assignments, where * covers the dynamic segment.
actionBlock, Drop, Mock, or Script — see below.
maxRunsOptional 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 optional body and contentType. 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-tier scripts capability, 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 valueBehaviour
empty (default)Unlimited — fire on every match, the same as before this feature existed.
1Fire once, then auto-disable. The token-refresh trick.
NFire 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 enabled to false for 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.

global rulemaxRuns: 1Phone APhone BLaptop Cfired once →suppressed herefired once →suppressed herenot yet matchedstill live

Setting the cap

maxRuns is settable at creation, the same two ways as any other field:

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_device MCP 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:

FieldValue
enabledtrue
methodGET
patternapi.doordash.com/v3/dashes/*/active_assignments
actionBlock (status 403)
json
{
  "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:

FieldValue
enabledtrue
methodGET
patternapi.doordash.com/v3/dashes/*/active_assignments
actionMock (status 200)
json
{
  "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:

json
{
  "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.