I want to… — find the right tool
Every name in bold below is written exactly as it appears in the extension, so you can also find it with Ctrl+F here or search for it in the interface.
…change what a request or a response does
All of these are a rule. Pick the rule type from the Rule type dropdown when you create it.
| I want to… | Use this |
|---|---|
| Make an endpoint return a 500, a 404, a 401 — an error I can't easily trigger for real | Rule type Mock the response (API), then set Status code. See Your first rule. |
| Return test data for an endpoint that doesn't exist yet | Rule type Mock the response (API), write the JSON in Response body. See Your first rule. |
| Start a mock from what the server really returned, instead of typing a fixture | The Mock button in the DevTools panel — see Building rules from real traffic. |
| Change one field of a real API response and leave the rest alone |
Rule type Modify the real response body, Mode =
Overwrite a JSON field, e.g. Field path user.status.
|
| Swap a piece of text inside a real response | Rule type Modify the real response body, Mode = Find and replace. |
| Add, override or remove a header | Rule type Modify headers — see the syntax and the Presets below. |
| Send an Authorization header the app doesn't send |
Rule type Modify headers, preset Auth headers. It writes
Authorization: Bearer {{token}}, so define a token
variable to go with it.
|
| Stop the browser serving from cache so my rule gets a chance to run | One-off: ↻ Reload without cache in the popup. For every reload: rule type Modify headers, preset Cache bypass. |
| Get past a CORS complaint while developing | Rule type Modify headers, preset Development CORS — but read Things that look like bugs but aren't first: it does not lift a real browser block. |
| Point a call at localhost or at another environment | Rule type Redirect the request, fill in Redirect to. |
Replace staging with local in every URL, keeping the rest |
Rule type Replace text in the URL (Find / Replace with). |
Send every /api/v1/… call to /api/v2/…, keeping the rest of the path |
Set Compare to regex, capture what you want to keep with parentheses, and
write it back with $1, $2… Pattern /api/v1/(.*) and destination
/api/v2/$1 is one rule instead of one per endpoint. The pattern tester under the URL field
shows you what each group captured, so you can check before saving. Works in Redirect to
and in Replace with; $$ writes a literal dollar sign, and a $N
with no group behind it becomes nothing rather than staying in the URL.
|
| Force a query parameter the interface doesn't expose (a debug flag, a page size) | Rule type Query parameter. |
| Make a call fail, to see what my error handling does | Rule type Block the request. |
| Test slow network, spinners and loading states | Set Latency (ms) in the rule's Network simulation block. It works on any rule type — including a rule that does nothing else. |
| Test against a flaky network, where every call takes a different amount of time | Min (ms) and Max (ms) in the same block. See Random latency. |
| See what my UI does when a call never answers | Simulated timeout (ms). Not the same as a long latency: the request ends up failing. See Simulated timeout. |
| Make an endpoint fail sometimes, not always — to test retries | Chaos mode: set a % errors or a % timeouts and the rest of the requests keep working. See Chaos mode. |
| See what my UI does if this endpoint returns a 500, but with the real payload and without writing a mock | Set Override the real status in the rule's Network simulation block. The request still goes out, the real body still comes back, and only the status code changes. See Overriding the status of a real response. |
| Send the backend a payload the interface would never send | Rule type Modify the request body. |
| Mock an endpoint and make it slow — or redirect a call and add a header to it | Two rules matching the same URL, one for each half: they all apply. See When several rules match the same request. |
| Affect a single GraphQL operation, not the whole endpoint |
Any rule type, plus a Body condition such as "operationName":"GetUser". The
DevTools panel fills this in for you.
|
| Limit a rule to one tab so it doesn't affect everything I have open | The 📌 button next to the rule in the popup — see extra conditions. |
| I have a cURL — from DevTools, a ticket, the docs — and I want a rule for that call | In the rule form, next to Pattern: Paste a cURL. See Pasting a cURL. |
| Point my whole app at my local backend instead of production | Point at another backend, next to the pattern field: two fields and done. See the shortcut. |
…find my way around the extension
The options page has two rows of controls, and which row something is on tells you what it affects. The header carries what governs everything: the project you are working in, then Export · Import ▾ · + New rule, and in the corner the master On/Off switch, the 🌐 interface language and the help.
The strip of tabs under it opens what lives inside the active project: 📁 Groups · 🎭 Scenarios · {{ }} Variables · 🌐 Environments · 🎯 Scope · 📡 Captures · 🗑 Bin. One panel is open at a time, and the one you leave open is the one you find open next time. They are in that order on purpose: the first five configure how your rules behave, and the last two configure nothing — 📡 Captures is the record of what your rules did, and 🗑 Bin is what you deleted.
| I want to… | Use this |
|---|---|
| Work out why my rule isn't doing anything | Why a rule might not be firing — the full checklist, in order. |
| Check my pattern matches a URL before saving the rule | Test URL (optional), right under the pattern in the rule form. See matching. |
| Know which of my rules wins when two of them match the same URL | When several rules match the same request — short answer: they all apply, and where they clash the one lower in the list wins. |
| See which requests a rule actually caught | Record this tab in the popup, then 📡 Captures in the options page. See Seeing what actually happened. |
| Put back something I have just deleted | 🗑 Bin — see The bin: undoing a delete. |
| Read the extension in another language | The 🌐 button in the header, next to the On/Off switch. English and Spanish; Automatic follows the browser. Your choice covers the popup, the options page, the DevTools panel and this help, it is not part of any project, and it does not travel in an export — it is a setting of yours, not of your rules. |
| Switch a host between Local, Staging and Prod without editing every rule | Variables plus Environments. |
| Turn several rules on or off together | Put them in a Group (the Groups panel at the top of the options page) and toggle the group. Or tick several rules in the list — by hand, or with Select all, which acts on whatever the filter is showing — and use the bar that appears. See Acting on what the filter is showing. |
| Have the five error states of one endpoint ready without typing five mocks | 🎭 Scenarios in the options page → + From template. Pick Typical HTTP errors or REST collection, say which URL, and every variant arrives with its rule already written — status, headers and body. None of them is switched on until you pick it. |
| Find one rule among many | The four controls above the list: the search box, and the filters by type, by group and by scenario. See Finding one rule among two hundred. |
| Mock a whole API I have the spec for, without writing each rule |
Import in the options page, and pick the openapi.json — see
Starting from an OpenAPI / Swagger spec.
|
| Put the response examples I already saved in Postman to work | The same Import, with the collection file — see Bringing in a Postman collection. A collection with no saved examples produces no rules at all, and that section says how to save them. |
| Get my ModHeader profiles back, now that the extension is gone from the store | The same Import, with the file you exported from there — see Bringing in a ModHeader profile. |
| Find out which files Import accepts, without trying them one by one | The ▾ next to it lists them. The menu informs, it does not route: every entry opens the same file picker and the file is recognised by its content, so picking the wrong entry breaks nothing. Pressing Import itself still goes straight to the picker, with no menu in between. |
| Share my setup with a teammate, or move it to another machine | Export / Import in the options page. Read what "sanitize on export" covers before sending the file. |
| Hand someone a trace of what the page did | Export HAR in the DevTools panel — see Building rules from real traffic. |
| Copy a request into a terminal or the console | Copy as… in the DevTools panel (cURL or fetch()). |
| Reuse my rules in my Playwright or Cypress tests | Copy as… on the rule's own row (just that one), or Export → the mock buttons in the dialog (all of them, or the selected ones). Read the comments in the snippet before you commit it: they say what was redacted and what could not be translated. |
| Call the endpoint a rule intercepts, to compare against the real thing | The same Copy as… on the row, options cURL and fetch(). |
| Make one rule win over another that also matches | Move it with ↑ and ↓: the one lower in the list wins. See When several rules match the same request. |
| Keep the extension off everything but my own sites | Scope in the options page: everywhere, only localhost, or only the domains you list. |
| Turn a whole navigation into mocks in one go | In the Net Tinker DevTools panel, tick the requests you want and press Save as scenario: one mock rule per request, with the status, headers and body that actually came back. Before creating it, the dialog shows you the list of rules that will be born: repeats of the same request are merged into one —keeping the last response— so a poll that fired twenty times does not leave you twenty identical rules. The scenario is created switched off and with a single variant, "As recorded" — activate it when you want it to apply, and duplicate that variant to write the "and now it fails" version on top. |
| Keep a recorded mock working when the parameters change |
When you save the recording, tick Ignore the URL parameters. Without it, a rule
recorded on /api/orders?page=2 is exactly that URL and answers only that
one: the moment your app sends a different parameter — another page, another tracking id — the rule
stops applying and nothing explains why. With the box ticked you get one rule per
endpoint, and the dialog's summary shows you how many are left before you accept. Your call:
if what you are testing is precisely the difference between ?page=1 and
?page=2, leave it unticked.
|
| Know what permissions it asks for, and what for |
In Scope, at the bottom: What it asks Chrome for, and what for. There
are two, counted from the manifest itself — not from a hand-written list that can go stale. And there is
the half you cannot see from outside: where the scope does not reach, the extension steps aside and
leaves fetch and XMLHttpRequest exactly as they were.
|
| Read a JWT without pasting it into someone else’s website | If a variable’s value is a token, its row shows a JWT badge — in red and saying expired when it is, which is usually the answer you are after. Clicking it shows the claims, the algorithm and the expiry date in plain words. It is decoded here, nothing leaves your computer; the signature is not checked, because without the issuer’s key there is no way to. |
| Stop the extension touching one site that behaves oddly | Open the popup on that site and press Exclude this site. It goes to the Never act on these domains list in Scope, which wins whatever the mode is — you keep everything else exactly as it was. Reload the page afterwards: what was already patched stays patched until then. |
| Turn everything off for a moment without deleting anything | The master toggle: On in the popup, Extension enabled in the options page. |
Your first rule: which fields actually matter
Press + New rule (in the popup or in the options page). The form opens on Mock the response (API), which is the most common case: answer a request from the browser instead of letting it reach the server. What the form shows up front is short — the rest is folded into four collapsible blocks, described below — and most of it is optional. Here is what a working JSON mock actually needs:
-
URL / pattern — the only field the extension refuses to save without. By default it is
read as Contains, so
/api/usersis enough to catch every URL with that fragment. See Choosing which requests a rule matches. -
Status code — starts at
200. The field is a plain number box with a list of suggestions attached (200, 201, 204, 301, 400, 401, 403, 404, 429, 500, 502, 503…): click it to pick one, or just type any code you want. -
Headers — a new mock rule opens with
Content-Type: application/jsonalready written in the box, as a real value you can edit or delete: what is in the box is what gets saved. Delete it and the mock comes back as plain text — a legitimate choice, and nothing writes the header back when you save. Opening a rule that already exists never adds it, even when its headers are empty. - Response body — what the caller gets. The { } Format and { } Minify buttons above the box tidy up JSON, and a note underneath tells you whether what you typed parses as JSON at all.
Everything else narrows the rule down or helps you find it later, and lives in four collapsible blocks: Organization (description, tags, group), Advanced matching (source, match type, method), Additional conditions (optional) (header, body and initiator conditions) and Network simulation (latency and status code, which apply to every rule type). A new rule opens with the four of them closed; editing an existing one opens each block that rule actually uses, so a folded block never hides something you set. Two things worth doing before you close the form:
- Paste a real URL into Test URL (optional). It tells you straight away whether this rule would match it, without saving anything or making any request.
- Save, reload the page, and look at the extension's toolbar icon: it shows a count of rules applied on that tab. If it stays empty, the rule never fired — go to Why a rule might not be firing.
Shortcut: point your whole app at another backend
The most common case of all — I want this to talk to my local server instead of production — does not need you to type a pattern. In the rule form, next to Pattern, there is Point at another backend: write the source host and the target host, press it, and the rule is done — pattern, type Replace text in the URL and both replacement fields.
What it writes is an ordinary rule, so you can edit it afterwards. And it affects every call to that host, which is exactly what you want here: if you only wanted one, use the DevTools panel.
Rule types
Every rule has exactly one type, which decides what happens once it matches a request. The name in bold is the one in the Rule type dropdown, where the nine of them are grouped by what they do to the request — Mock, Modified passthrough, Redirect and Block, the split described in Mock vs. modify & passthrough. The shorter name in brackets is how the same type is labelled in the rule list, the type filter and the captures list. One rule does one of these things — but a request can be governed by several rules at once, so combining two types is a matter of writing two rules. See When several rules match the same request.
| Type | What it does | When to use it |
|---|---|---|
| Mock the response (API) [Response] |
The request never reaches the network. You fabricate the status, headers and body yourself. | The backend endpoint doesn't exist yet, or you need a specific error/edge case (500, empty list, malformed data) that's hard to trigger for real. |
| Modify the request body [Request body] |
The request still goes out for real, but with a different body than the page sent. Two modes:
replace the whole body, or overwrite one JSON field by dot path
(variables.userId) leaving the rest of the body exactly as the page built it. The new
value is read as JSON when it is valid JSON (42, true, null,
{"a":1}) and as plain text otherwise. If the body isn't JSON — or isn't text at all, like
a FormData — the request goes out untouched and the Activity panel says the rule matched
but couldn't be applied.
|
Testing how the backend reacts to a payload the UI wouldn't normally send. The one-field mode is
what makes this usable with GraphQL: you can change variables.userId
without pasting the whole query into the rule — and without the rule going stale the next time the
front end adds a field to that query.
|
| Modify headers [Headers] |
Add, override or remove request and/or response headers. The request still goes out for real. One per
line: Key: Value overwrites or adds, +Key: Value appends to what is already
there, -Key removes. The Presets dropdown fills the boxes in for you with
three common sets — Cache bypass, Development CORS and
Auth headers — adding to whatever you already wrote and only replacing lines for the
same header.
|
Forcing a feature flag header, testing a missing/extra CORS header, faking an auth header locally. |
| Redirect the request [Redirect] |
Sends the request to a different URL entirely. | Pointing a call at a local server or a different environment without touching the app's config. |
| Block the request [Cancel] |
The request fails outright — fetch rejects, XHR fires an error event. |
Testing what your UI does when a call fails or a script fails to load. |
| Query parameter [Query param] |
Add or remove query string parameters. The request still goes out for real. One per line:
+key=value adds or overwrites, -key removes. |
Forcing a query param the UI doesn't expose (a debug flag, a page size). |
| Replace text in the URL [Replace] |
Replaces every occurrence of a piece of text in the URL, keeping the rest intact. The request
still goes out for real. The text you type in Find follows the rule's own
match type: with the other three it is matched literally, character for character, so
a . only ever matches a real dot. Choose Regex and it becomes a real
regular expression, and Replace with can use $1…$N for its
groups — a group that matched nothing becomes empty, never a literal $1.
|
Swapping staging for production in a URL without a full redirect rule. |
| No change (delay only) [No change] |
Does nothing to the request by itself: it goes out exactly as the page built it. It exists so that a rule can carry only a modifier — today, a latency — without also having to change something. | Slowing an endpoint down without altering it in any other way. |
| Modify the real response body [Response body] |
The request goes out completely unchanged; only the body that comes back is transformed before your
code sees it. Two modes: Find and replace (literal text, every occurrence) or
Overwrite a JSON field (a dot path such as user.status, leaving the rest
of the JSON alone). It has limits of its own — see
Things that look like bugs but aren't.
|
Flipping one field in a real API response (e.g. a status) without hand-writing a full mock. |
Latency is not a rule type
Delaying a request used to be a type of its own, and it no longer is. Latency (ms) lives in
the Network simulation block of the rule form and applies to every type: a mock
arrives late, a redirect leaves late, a blocked request fails late. Leave the field empty and the rule adds
no latency at all; 0 is not the same thing as empty — it is a real, if tiny, wait.
If all you want is to slow an endpoint down, use the No change (delay only) type and give it a latency. Rules that used to be of the old Delay type were converted to exactly that, keeping their milliseconds, the first time you ran this version.
Network simulation: random latency, timeout and chaos
The Network simulation block of the form holds four ways to make a request misbehave, and all four work with any rule type — a mock, a redirect or a No change (delay only) rule take them just the same. Every field is optional, and empty means "not this one". The fixed Latency (ms) is explained just above; here are the other three.
Random latency
Min (ms) and Max (ms), the row underneath the fixed latency. Every request waits a different amount between those two values, both ends included: with 100 and 200 you can get 100, you can get 200 and you can get anything in between, and it is drawn again on every request.
- You need both. With only one there is no range to draw from, so the rule behaves as if there were none and the fixed Latency (ms) is what applies.
- When both are set they replace the fixed latency; they don't add to it. The fixed one stays saved in case you clear the range, but it is not used while the range is there.
- If you write them the wrong way round (500 and 100) it is read as the range 100–500. Rejecting it would leave the rule with no latency and no explanation.
Simulated timeout
Simulated timeout (ms) waits that long and then makes the request fail, the way a real network timeout does. It sits next to latency in the form and it is not the same thing:
| This… | …does this to the request |
|---|---|
| Latency (ms) | Succeeds, just late. Your code gets a response. |
| Simulated timeout (ms) | Waits and then fails. Your code gets a network error, not a response. |
| Type Block the request | Fails immediately, without waiting for anything. |
- It beats whatever the rule does. A rule that mocks and carries a timeout never delivers its mock: promising that no response arrives beats promising one. The only thing that beats a timeout is the Block the request type.
- It does not add to the latency. It waits exactly the milliseconds you wrote, not latency plus timeout: a 3000 timeout with 200 ms of latency would fail at 3.2 s, and nobody asked for that 0.2. The waiting time being the number you wrote is the only thing that makes a timeout measurable.
Chaos mode
Three fields — % errors, Error status and % timeouts — that roll the dice on the outcome of every request. The success rate is not asked for: it is whatever is left over. With 20 and 10, two requests out of ten get the error, one out of ten gets no response, and the other seven do exactly what the rule says.
-
The error is served as a local response, with the Error status you set
(
500if you leave it empty) and no body and no headers: chaos has no idea what body would make sense, and making one up would invent a contract your page never agreed to. - The chaos timeout waits for whatever the rule already declared: its Simulated timeout (ms) if it has one, and failing that its latency. With neither of them it fails as soon as a turn passes, which is the only honest thing to do when nobody said how long to wait.
- If the two percentages add up to more than 100, the error is served first and the timeout gets whatever is left up to 100. There is nothing to fix: that is what the extension does so an impossible sum degrades predictably instead of breaking.
- Where it shows most is on a Mock the response (API) rule: there chaos beats the rule's own mock, which is exactly what "this endpoint fails 20% of the time" means.
Call limit and intermittent failure
The only two fields in the whole extension that count requests. Everything else looks at the request in front of it; these two also look at how many that rule has handled already.
| This… | …does this to the request |
|---|---|
| Call limit |
The first After N calls go through as usual; from the next one on, the rule answers with
the Status you set (429 if you leave it empty). It is what a server does
when it cuts you off for calling too much.
|
| Intermittent failure |
Fails the Nth request and lets the rest through: with 1 in every 3, the 3rd, 6th and 9th
fail, with the Status you set (500 if you leave it empty). The first
request never fails, so you get to see the rule work before you see it break.
|
Where the count comes from, which is what you need to know before using either:
- It is per page, and reloading starts it over. That is not a limitation in disguise: reloading is exactly the gesture of someone testing a limit, and without it there would be no other button to start again.
- The same page open in two tabs counts twice, and each iframe counts on its own: every document runs its own engine, and none of them sees what the others do.
- The count is never stored. Close the tab and nothing is left: a limit you tried on Tuesday cannot still be used up on Thursday.
- Editing another rule does not touch it. Deleting this one does: a new rule starts from zero even if you give it the same name.
- The requests it cuts never leave your browser, so they appear neither in DevTools' Network tab nor in the Net Tinker panel — both of them list what actually went out. Ten calls with a limit of 3 show up as three rows there, and that gap is the proof that the other seven were stopped. To see all ten, turn on Record this tab in the popup and open 📡 Captures. The panel tells you this itself while a rule like this is active.
If you set two at once — or either one together with chaos — the most predictable one wins: first the Call limit, then the Intermittent failure, and Chaos mode last. Once the limit is crossed there is no dice roll left: what you can predict by reading your rule beats what nobody can predict.
503. The first call works, the second
fails, the third works again — that shows whether your code retries and whether the second attempt gets
through. With the Call limit at 3 and 429 you see the opposite: what your UI
does when something stops working and does not come back.
The page console
Log to the page console when this rule matches is the last checkbox in the block, and the
only one that does nothing to the request: it goes
out exactly as it would have. What it adds is one [Net Tinker] line per matched request in the
page's own console — the rule's name, the method, the URL and what happened to it — right next to your
application's own logs. That is the whole point: the same information already lives in
📡 Captures, but in another window, and while you are chasing a bug you are reading the
console, not switching tabs.
The page sees it too, and that is worth knowing before you tick it. Net Tinker writes this
line from inside the page, so anything the page does with its console — an error reporter, a session
recorder, a screen you are sharing — gets it as well. For that reason the line never contains headers, body
or variable values: your auth preset is literally Authorization: Bearer {{token}}, and a token
has no business in a console. If you need to see what a rule actually applied, that is what the capture
detail is for.
Overriding the status of a real response
There are two fields in the form that hold an HTTP status code, and they are not the same thing. It is the easiest confusion to fall into here, so it goes first:
| Field | What it is | Where it lives |
|---|---|---|
| Status code | The status of a response the extension builds. The request never goes out, so there is no real response anywhere. | Only on the Mock the response (API) type, next to the mock's body and headers. |
| Override the real status | Replaces the status of the response that did come back from the server. The real body, the real headers and everything else arrive untouched. | In the Network simulation block, on any rule type. |
The case it exists for: you want to see what your error handling does with a 500 from that
endpoint, but with the real data. With a mock you would have to hand-write the whole body — and then you
would no longer be testing against the real response, which is exactly what you wanted to keep.
-
200to599is what's accepted. Outside that range the browser cannot build the response at all, so an out-of-range value is ignored rather than breaking the request. -
The status text is left empty. A
500still carrying the original response'sOKcontradicts itself the moment you log it; and an empty status text is what the browser already hands you over HTTP/2 and HTTP/3, which have no reason phrase. -
With
204,205or304the body is dropped, because those statuses cannot carry one. That is exactly what the browser does with a real response of that kind. - Leave the field empty to keep the real status: that is the only way to remove an override.
- On a rule that mocks or blocks the request it does nothing, and that is not a bug: there is no real response to override there. In the Activity panel such a rule shows up as matched but not applied. For a mock, the field that counts is Status code.
And like any modifier, it combines: a single rule can override the status and transform the response body, and change a header on it, and arrive late.
Mock vs. modify & passthrough
This is the single most common point of confusion, so it's worth spelling out on its own:
Response
(or fakes the XHR properties) and hands it straight back. The real server never even sees the request.
A practical way to tell them apart while debugging: open DevTools' Network tab. A mocked request never shows up there at all (it never reached the network); a modified-and-passed-through one does, with the rewritten URL/body/headers. The captures list says the same thing in words, per request: "reached the network" or "never reached the network".
This is a property of the request, not of each rule taken alone: as soon as one matching rule mocks it, the request never leaves the browser, whatever the other matching rules do. A mock with a latency is a delayed answer that still never reaches the server — see When several rules match the same request.
Choosing which requests a rule matches
Whatever its type, a rule always starts from one pattern: the Source dropdown decides whether that pattern is compared against the full URL or only the Host, and the Match type dropdown decides how. Both live inside the form's Advanced matching block, which is folded away until you open it:
| Match type | How the pattern is read |
|---|---|
| Contains (the default) | Matches if the pattern appears anywhere in the URL/host. /api/users catches every URL with that fragment in it. |
| Equals | Matches only if the URL/host is exactly the pattern, character for character. |
| Regex | The pattern is a JavaScript regular expression, tested against the URL/host. It doesn't have to match the whole thing unless you anchor it with ^/$. |
| Wildcard (* ?) |
A middle ground between "Contains" and "Regex": * stands for any run of characters
(including none) and ? for exactly one character. Everything else is literal, so you don't
have to escape anything — a . is a real dot, not "any character". Like Regex it isn't
anchored either: the pattern only has to occur somewhere in the URL/host.
|
https://*.example.com/api/v?/users in Wildcard mode matches
https://api.example.com/api/v2/users and https://staging.example.com/api/v3/users,
but not https://example.com/api/v10/users — ? is exactly one character.
Rather than guessing, paste a real URL into Test URL (optional), the box right under the pattern. It answers ✓ Matches or ✗ Doesn't match as you type, tells you what it compared against (the whole URL or just the host), and warns you if a Regex pattern doesn't compile. Nothing is saved and no request is made. It decides with the pattern, the match type, the source and the exclusions; the method and the header/body/initiator conditions it names instead of judging — a URL carries no method and no body, so it lists them as things it could not check rather than pretending it did.
Everything under /api/ except the health check
A pattern that is broad enough to be useful is usually broad enough to catch something you didn't want: the health check your dashboard polls every five seconds, the login call, the one request that already carries a real token. That's what Except URLs containing (one per line) is for — it sits just under the three dropdowns in Advanced matching. Write one pattern per line; if any of them appears in the request's URL, the rule leaves that request alone.
/api/ with /api/health and /api/auth/ as exclusions: every
API call is mocked except the health check and anything under auth.
Two things about this box are deliberate, and worth knowing before you fight it:
-
It is always compared against the whole URL, even when Source is set to
Host. That's what makes it usable on the rules that need it most: a rule scoped to a host would otherwise
have nothing but a hostname to compare against, and
/api/healthnever appears in one. It also means you can exclude a whole subdomain here (static.example.com) as easily as a path. -
It is always plain text, whatever Match type says. No regex, no
wildcards: a
.is a dot and a*is an asterisk. Case matters, as everywhere else. If you genuinely need a regular expression to describe what to leave out, the old escape hatch still works — a negative lookahead in the pattern itself,^(?!.*health).*api.*in Regex mode.
The Test URL box knows about exclusions: an excluded URL answers ✗ Excluded by “…” and names the line responsible, instead of claiming a match that would never happen.
On top of the pattern, a rule can carry extra conditions, under Additional conditions (optional) in the form — except Method, which sits with the other two dropdowns under Advanced matching. They are all optional, and all of them have to hold at once (AND, never OR):
- Method — anything other than "Any" restricts the rule to that verb.
- Header condition (optional) — a header name plus is present / is absent / equals / contains / matches regex.
- Body condition — Contains / Equals / Regex against the outgoing request body. (Wildcards are offered for the URL and initiator patterns, but not here.)
-
Initiator domain (optional) — the page making the request, not the request's
target, with the same four match types as the URL pattern. The pattern is tried against the page's
origin (
http://localhost:3000) and against its bare host (localhost), and either one matching is enough — so a pattern you wrote before keeps working, and you can now tell two dev servers on different ports apart, which the bare host can never do. To pin one port:localhost:3000with Contains, or the whole originhttp://localhost:3000with Exact.localhoston its own still means both. - Pinned to a tab (📌 in the popup) — the rule only applies to requests coming from that one tab.
Finding one rule among two hundred
There are four controls above the list, and they combine: what's left is what satisfies all of them at once.
- The search box. It says "name, URL or group", but it also looks inside the Description and the Tags. Deliberately the same criteria as the popup's search box: searching "staging" has to find the same rule in both places.
- Filter by type — only mocks, only redirects…
- Filter by group. Besides your groups it offers Without a group, which is where everything created in a hurry ends up. It only shows up once you have at least one group.
- Filter by scenario. You can pick a whole scenario (any variant of this scenario) or one variant, and also Outside every scenario: the rules no variant governs. That also catches rules pointing at a variant that no longer exists here — typical of a file imported from another machine — which will never apply; the list flags them with ⚠. It only shows up once you have a scenario with variants.
Each rule shows its group at the start of its row, in a tag before the name. If the group is disabled the tag carries a ⚠ and the row is dimmed. Rules with no group carry no tag at all: to find them, use the filter.
What to keep in mind when filtering. The order of the list is the priority order (the lowest matching rule wins), and filtering does not change it: what remains keeps its real order. But a rule the filter hides still competes, so with a filter on, "the last one I can see" isn't necessarily "the one that wins". That's why, as soon as a filter hides rules, a line appears above the list saying how many of how many are showing and reminding you of this — with a button to clear all four controls at once. If you're deciding priorities with the ↑ and ↓ arrows, clear the filters first.
Acting on what the filter is showing
Next to the search box and the three filters there are two more buttons, Select all and Select none, and they act on whatever the filter is showing: with a filter on, only on those rules; with no filter, on all of them. The ☑ in each group, over in the Groups panel, respects the filter the same way. A line under the two buttons says so in plain text rather than leaving it to a tooltip — which does not exist on touch, and does not show up when you tab to the button.
A selection deliberately survives a filter change: searching, selecting, searching for something else, selecting more and then acting on the union is the whole point of those buttons. So the counter in the selection bar carries a denominator and also says what is out of sight — "30 of 200 selected · 30 outside the filter" — and deleting asks with the subtraction done: "Delete 30 of your 200 rules? 170 will remain." A single number can be misread two opposite ways ("I thought that was all of them", "I thought that was just what's on screen") and the subtraction closes both. The ✕ in the bar clears the whole selection, which is what makes it different from Select none.
When several rules match the same request
Nothing stops two rules from matching the same request, and when that happens all of them apply — not just the first one. That is what lets you redirect a call and add a header to it, without trying to cram both into a single rule.
They are applied in list order, from top to bottom, and each rule only has a say in the part of the request its own type is about: a headers rule contributes header changes, a mock contributes the answer, and any of them can contribute a latency on top. When two rules speak about the same thing, the one lower in the list wins. So the order of the rule list is the priority — use the ↑ and ↓ buttons on a rule to move it above or below another one, and what your request ends up doing really does change.
| Two matching rules both… | What the request ends up doing |
|---|---|
| …mock the response | The lower one answers: its status, its headers, its body. The upper one is replaced outright, not blended into it. |
| …rewrite the URL (redirect, query parameter, replace text) |
Both, chained: each one rewrites the URL the previous one produced. "Redirect to localhost" plus "add
?debug=1" gives you a localhost URL with the parameter on it, not one or the other.
|
| …modify headers | Both sets of lines, run one after the other in list order — so for the same header name the later line wins, exactly as two lines inside a single rule would. |
| …modify the real response body | Both, in order: the lower rule transforms whatever the upper one produced. |
| …set a latency, or modify the request body | The lower one's value. A latency of 0 ms and an empty body are real answers, not "no opinion". |
/api/users with a 500 and carries a 2000 ms latency gives you a
500 that takes two seconds to arrive. Two separate rules — one mocking, one only adding the latency — give
you exactly the same thing, which is handy when you want to switch the slowness off on its own.
Two combinations are settled by a rule of their own rather than by list order:
- Blocking always wins. If any of the matching rules is a Block the request, the request fails — wherever that rule sits in the list, and whatever the other rules say. A block plus a mock is a failed request, never a mocked one; if a latency is in play too, the failure simply arrives late. That is the answer to "I have a rule that blocks and a rule that answers, why does it fail?": move the block out of the way (disable it, or narrow its pattern) if you want the mock.
- Nothing that acts on the way back applies to a mock. A mocked request never reaches the network, so there is no real response for a Modify the real response body rule — or for the response-header side of a Modify headers rule — to work on, even when it matched the same request. Write what you want directly into the mock's own body and headers instead.
To see which rules governed a given request rather than reasoning it out, record the tab and open the captures list: a request that matched three rules gets a ⚡ line for each of them, and the toolbar badge counts rules applied, not requests.
Projects: separate jobs that never mix
A project is everything you have set up for one job: its rules, its groups, its scenarios, its variables and its environments. What matters is not that they sit together, but that only the active project applies: rules in the other ones never touch a request, even when they are switched on.
There is one from the moment you install the extension, and while it is the only one you need do nothing: its name sits at the top, next to the page title, and everything you have is in it. Click it to switch projects, create another, rename it or delete it. The popup grows a Project dropdown as soon as there is more than one.
Everything you create lands in the active project, imports included: a pack, an OpenAPI spec, a Postman collection or a HAR all come in where you are working.
And what you export is the project you are in, only that: its rules, groups, scenarios, variables and environments. Nothing from your other projects travels in the file.
The pack says which project it came from (its name and its scope), and when you import it you choose where it goes: into the project you are in, or into one with that name — created if you don't have it, and if you do, what you import is added to whatever is already there. That is how you move a client from one machine to another in one piece. The destination always starts at "this project": creating one is your decision, never a side effect. If the pack came from the main project — the one with no name of its own — you are asked nothing, because there is no name to offer you.
What else belongs to each project, which tends to surprise the first time:
-
Scope (where the extension is allowed to act). One project can be limited to
localhostwhile another acts everywhere. A new one starts unrestricted; it does not inherit the previous one's restriction. - The active environment: switching project can change what your variables are worth.
- The trash and the activity log. What you delete in a project is restored from that project, and recorded traffic stays where you recorded it. Emptying either one empties the one you are looking at, not all of them.
One hint for when something is not applying: if a rule you are sure you created is nowhere to be found, look at the name up top. It is almost always in the other project. That is the first place to check, even before the ones in Why a rule may not be applying.
Using variables
Define a name/value pair once (the "{{ }} Variables" button in the options page) and reuse it as
{{name}} anywhere you'd otherwise retype the same string: a rule's body, a header's value, a
redirect URL, a query param's value, the replacement text of a "Replace text in the URL" rule, and both the
replacement text and the new JSON field value of a "Modify the real response body" rule.
apiHost = api.staging.example.com, then use https://{{apiHost}}/v2/users
as a redirect target. Change the environment once, in one place, instead of hunting down every rule that
hardcoded the old host.
A name that doesn't match any defined variable is left exactly as written — {{typo}} shows up
literally in the request or response, rather than silently disappearing, so a mistake is obvious instead
of a mystery. Variables are not substituted into a rule's matching fields (the URL
pattern, or a header/body/initiator condition) — those compare against the real, live request, so a
variable there wouldn't mean anything.
Reading and copying what a JWT carries
When a variable's value looks like a JWT, a JWT badge appears next to it. Pressing it opens the viewer: the header and the claims, with the expiry as a wall-clock date rather than the number of seconds the token carries.
The viewer has two buttons, and they do deliberately different things: Copy token copies
the token as it is, ready to paste into an Authorization header;
Copy claims copies the JSON of the header and payload, which is what you paste into a
ticket or a chat.
The viewer does not check the signature, and it cannot: that needs the server's key. It tells you what the token says, not whether the server will accept it.
Environments
An environment (the "🌐 Environments" button, next to Variables) is a named set of overrides on top of your variables — Local, Staging, Prod, whatever you need. It only has to override what actually changes between environments; anything it doesn't mention keeps the variable's own base value.
apiHost = local.test as the base value, a "Prod" environment can override just
apiHost = api.example.com and leave every other variable alone.
Pick the active environment from the star button next to it in the options page, or from the Environment dropdown in the popup (only shown once at least one environment exists). With no active environment, every variable simply uses its own base value — exactly like before environments existed.
Scenarios: several versions of the same API
A scenario groups several named variants of the same API, of which exactly one is active: Login → Success / Invalid credentials / Backend down. A rule can belong to a variant, and the rules of a variant that isn't active are not applied — the engine never even looks at them.
Exclusion is per API, not global. Two scenarios can be in different states at the same time: Login on "Backend down" while Payments stays on "Success". That is the difference from keeping loose rules and switching them on and off by hand, which is what this replaces.
You create and organise them on the options page, under 🎭 Scenarios; you switch the active variant there or — handier while you are testing — from the popup, which shows one row per scenario. A rule is assigned to a variant from its own form.
If you'd rather not start from an empty list, + From template leaves the scenario built, with its variants and their rules already written: the typical HTTP errors, or the skeleton of a REST collection. And scenarios travel in the export file along with the rules, so you share them with your team like everything else; on import, one that collides with a scenario you already have asks whether to overwrite, duplicate or skip, just like rules and groups.
Picking a variant that no longer exists — because someone deleted it — stores "none" rather than a dangling pointer: the rule stops belonging to anyone and behaves like a loose rule.
Deleting a scenario, or a single variant, asks what to do with its rules instead of deciding for you. The dialog says how many live inside and offers two ways out: Keep the rules, disabled — they come out of their variant and are left switched off, so none of them starts firing on its own now that nothing governs it — or delete them too, which cannot be undone. With no rules inside, the question is the short one. It is the same treatment deleting a Group with rules in it already got.
Writing a variable from a response
The other way round from everything above: instead of putting a value into a request, a rule can
take a value out of the response and store it in one of your variables. From then on that
variable interpolates everywhere the previous section lists, so the typical case is one rule: extract
token from your /login response into the JWT variable, and every
rule sending Authorization: Bearer {{JWT}} starts sending the freshly issued token — no more
copying it by hand.
It lives in the "Write a variable from the response" collapsible block of the rule form, and it does nothing until you pick a variable: none of your existing rules capture anything. There are two ways to locate the value:
-
A JSON field, by its dot path — the same syntax as "Overwrite JSON field".
data.tokenreadstokenout of{"data":{"token":"…"}}. - A regular expression, for when the response isn't JSON or the value sits inside something else. The first capture group is stored; if the pattern has none, the whole match is.
/api/login that changes nothing about the response ("No change" is fine), with
Write into = {{JWT}} and Field path =
data.token. Then another rule on /api/, type "Modify headers", adding
Authorization: Bearer {{JWT}}. Log in once and the rest of the session is authenticated.
Where the value ends up, which is worth knowing before you rely on it: in the browser session's memory. It is not written to disk, it never appears in an exported file — and there is no checkbox to include it — and it is gone when you close the browser. Next time you open it the variable is back to whatever your variables list says (usually nothing) until a response refills it: logging in again is enough. The "Clear captured values" button in the Variables panel discards them right away, without waiting for anything to close.
A captured value wins over the one written in your list and over an environment's override, but only inside the environment it was captured in: a token taken in "Staging" is not used once you switch to "Production", where the variable goes back to its usual value.
One limitation to keep in mind: the value is not available to requests fired at that same instant. It travels through the extension's background first — milliseconds, which the normal case (log in, then everything else) doesn't notice; what does notice is firing two requests at once and expecting the second one to already carry the first one's token.
A failed extraction does no harm: the response reaches the page whole and untouched, and the Activity panel marks that request saying which of the two things happened — that there was nothing to extract there (the path or the pattern found nothing in that response), or that it could not be extracted (the body isn't JSON, the regular expression is invalid, the field exists but is an object). Binary and streaming bodies are never read, and that is reported too.
Why a rule might not be firing
First, a quick check that saves a lot of time: the extension's toolbar icon carries a badge with the number of rules applied on the current tab. If it's empty after reloading, nothing matched at all and the list below is where to look. If it's counting, the rule is firing and the surprise is in what it does, not in whether it runs.
And a shortcut for half of this list. Open the rule and paste the request into Test URL: besides telling you whether the pattern matches, the box lists what would stop that rule from applying —the master switch, its own, its group, its scenario variant, a rule type the engine no longer applies— and, separately, what it can't decide from a URL: the method, the header, body and initiator conditions, the scope, and the tab it may be pinned to. Whenever there is something to explain, the box ends by saying what it can't see, which is precisely the tail of this list.
Check these in order — they cover every reason a rule can silently fail to apply:
- The master toggle (On at the top of the popup, Extension enabled in the options page) is off. Nothing applies at all while it is.
- The rule's own enable/disable toggle is off.
- The rule belongs to a group that's disabled. A disabled group silently disables every rule inside it — in the rule list, those rules show a ⚠ next to the group's name.
- The rule is pinned to a specific tab (📌) and you're testing from a different one.
- This site is outside the configured scope. This is the most misleading one, because it has nothing to do with the rule: if Scope (options page) is set to "Only localhost" or "Only these domains", then on every other site no rule applies at all. The toolbar icon says so — hover it and you'll read "out of scope here" — and in that mode the popup warns too. Watch out for the empty-list case: "Only these domains" with no domain listed allows nothing, anywhere.
- The request's HTTP method doesn't match the rule's configured Method (if it's not set to "Any").
-
The URL/host pattern doesn't actually match — check the match type as well as
the pattern itself, and paste the real URL into Test URL (optional) to settle it. If the
match type is Regex, a typo that makes the pattern invalid means the rule matches
nothing rather than throwing an error — the test box flags that case explicitly. Another common
one: writing
*or?in a pattern still set to "Contains" or "Equals", where they are just literal characters — those only mean anything in Wildcard mode. - The rule has an exclusion pattern that matches this URL. It's the easiest one to overlook, because the positive pattern does match and everything looks right: check Except URLs containing inside Advanced matching — the block opens by itself when the rule has any, and the folded summary says how many. Pasting the URL into Test URL (optional) settles it instantly: it names the line that excludes it.
-
There's a Header condition, Body condition or
Initiator domain attached to the rule, and the current request doesn't
satisfy it. For header and body conditions on XHR requests specifically: a redirect/query-param/
replace-in-URL rule can never fire based on either, because the URL is decided before the
page has set its headers or called
send(body)— this only works for the "mock", "block", "no change", "modify request body" and "modify headers" rule types on XHR (it works for every rule type onfetch). A body condition also only ever matches plain text (JSON, form-encoded, plain strings) — FormData/Blob/binary bodies are never read for it. -
The request isn't
fetchorXMLHttpRequest. Net Tinker can only intercept those two — it cannot affect<img>,<script src>, CSS, fonts, or any other resource type when it comes to applying a rule (the DevTools capture panel can still see all of those, it just can't act on them). Careful with the reverse reading: what counts is how the request was made, not what came back. An SVG or a PNG fetched withfetch()isfetch, and can be mocked. -
The request didn't come from the page, but from its service worker. Plenty of sites
register a service worker (you'll see it under DevTools → Application → Service Workers) that downloads
part of the content on its own. Those requests look ordinary — the browser counts them as
fetchand they show up in the Network tab like any other — but Net Tinker works from inside the page, and a service worker is a separate place no extension of this kind can reach. A rule on that URL is created, looks enabled, and never fires. There is no fix: that's how the browser works, not a bug. The DevTools panel does recognise those requests and tells you before you lose an afternoon to it. The same goes for whatever the page's workers request, fornavigator.sendBeacon, for WebSockets, and for preloads the browser resolves by itself. - The request never happened, because the browser answered it from its own cache. There is nothing to intercept, and it looks exactly like a rule that doesn't work. Use ↻ Reload without cache in the popup to reload that tab ignoring the cache, or add a Modify headers rule with the Cache bypass preset so it happens on every reload.
- Another matching rule is overruling it. Rules don't shadow each other — they all apply — but where two of them speak about the same thing, the one lower in the list wins, and a Block the request rule beats everything. So the rule can be firing perfectly and still leave no trace: reorder the list, or narrow the other rule's pattern. See When several rules match the same request.
- The rule has a type this version doesn't know how to apply. It can happen after importing a file written by a newer version, or if an update left a rule half-migrated. That rule alone is skipped, exactly as if it hadn't matched — never answered with an empty mock, and any other rule that matched the same request still applies — and the row is flagged in the captures list with ⚠ plus the unrecognised type name, so the situation is visible instead of looking like ordinary traffic. Reopen the rule and pick a type again.
- A Modify the real response body rule matched, but the response was a live stream it can't buffer, so it was deliberately left alone. See Things that look like bugs but aren't; the captures list says "matched but not applied — streaming response" for exactly this.
- You have two copies of Net Tinker installed at once — typically the Web Store one plus an unpacked development build. Both patch the same page, and whichever answers first takes the request, so the other copy sees nothing: no badge, no line in the captures list, and the rule you are looking at appears to do nothing even though a rule did run. The giveaway is that the request clearly was intercepted while the extension you're watching shows no activity at all. Disable one of the two.
Seeing what actually happened
Guessing whether a rule ran is the slow way. There are three places that tell you, each answering a different question:
- The toolbar badge — a live count of how many rules have been applied on the current tab. Costs nothing to look at and answers "is anything happening at all?".
- The captures list — a stored log of requests, with the rule (or rules) that matched each one. This is the one to open when something is wrong.
- The DevTools panel — live traffic while you work, and the place to build rules from it. See Building rules from real traffic.
The captures list is off until you ask for it. Open the popup on the tab you want to watch and switch on Record this tab — only that tab's requests are captured. Then open the list with 📡 Captures in the options page (or View captures in the popup), which shows the Captured requests panel.
Each row tells you, in this order: the page, the status, the time, and then what Net Tinker did with it.
- A ⚡ plus a rule name means that request matched that rule. Tick Applied only to hide everything else, and use the search box to narrow by URL or rule name. When several rules governed the same request each one gets its own ⚡ line, so the same URL appearing more than once is the list telling you exactly which rules took part.
- Every matched row also says reached the network or never reached the network, which is the quickest way to confirm a mock really is a mock.
- A ⚠ means the rule matched but could not be applied — an unrecognised rule type, or a streaming response a "Modify the real response body" rule had to leave alone. Both are explained in Why a rule might not be firing.
- View rule opens the rule that was applied; Disable this rule switches it off from here; + Rule builds a new rule from that request; and clicking the row opens the full Request detail, including the headers and what exactly was changed.
Unlike the DevTools panel, these captures are stored, so they're still there after you close the tab or the browser. The list keeps the 200 most recent and Clear empties it. Recording stops on its own if you close the tab being recorded.
The bin: undoing a delete
🗑 Bin keeps your last deletions so you can put them back. What it undoes is the gesture, not the item: deleting two hundred rules over a filter is one entry, and restoring it brings the two hundred back at once. It is also why deleting a group or a scenario with its rules inside comes back whole — the rules and their container went in together.
Every way of deleting is in there, and each row says which one it was:
- Rule deleted / Rules deleted — one rule, or a selection.
- Group deleted / Scenario deleted / Variant deleted — with whatever rules were inside.
- Variable deleted / Environment deleted.
- Replaced by an import — importing with Replace what is here sweeps away the rules, groups, scenarios, variables and environments of the project you are in — the others are left alone — and that whole sweep is a single entry.
It holds the last ten deletions, and at most 1 MB in total. Older ones drop off as new ones arrive — there is no expiry by days, on purpose: an extension's background worker is stopped after seconds of inactivity, so a bin that emptied itself "after a week" would only really empty itself the next time you happened to open the browser. Counting deletions needs no clock.
The 1 MB cap is there because ten deletions of one rule weigh nothing and ten of two hundred rules with mock bodies inside do. And a single deletion that on its own would not fit is not stored at all — the extension says so before deleting, so you can export first. An undo people believe they have and don't is worse than no undo.
Two things restoring will not do quietly, and it tells you when either happens:
- A rule whose group no longer exists comes back without a group. The group is not recreated: you deleted it afterwards and on purpose, and bringing it back would undo a gesture you never asked to undo.
- A rule whose scenario variant no longer exists comes back turned OFF. It was dormant because its variant was not the live one; switching it on as it returns would apply a rule you had not chosen to apply.
The bin is per project: it lists what you deleted in the project you are in, and what you deleted in another one is waiting in that one. Deleting a project itself does not go to the bin — it takes its rules, groups, scenarios, variables, environments, captures and its own bin with it, for good. The confirmation says so, and lists what you are about to lose item by item.
Things that look like bugs but aren't
Net Tinker works by patching fetch/XMLHttpRequest inside the page's own JavaScript.
That approach has a few unavoidable side effects:
-
A request whose response body nobody reads doesn't show up in the DevTools panel. Not in
Chrome's own Network tab either, and it is not the extension: a response whose body is
never consumed does not finish, so the browser never fires the event both lists are built on. Code
like
const r = await fetch(url); log(r.status);— checking the status and never touching the body — produces requests that reach the server, come back fine, and appear nowhere. If your page does that and a rule seems not to be working, look in 📡 Captures instead: the extension records what it did to the request, whether or not the browser ever calls it finished. - Adding a request header can trigger a CORS error. If the added header isn't allowed by the server's CORS policy on a cross-origin call, the browser blocks the request — this happens at the browser's CORS layer, underneath anything Net Tinker can control.
- The "Development CORS" preset does not lift a real CORS block. The browser runs its CORS check on the response the server actually sent, before Net Tinker gets to touch the response headers. The permissive values the preset writes are only ever seen by the page's own code. It is useful for testing how your code behaves with those headers; it is not a way around a server that doesn't send them.
- Response headers added by a rule don't show up in DevTools' Network tab. The Network tab shows the real bytes that came over the wire; a "Modify headers" rule's response-side changes are applied afterwards, only to the object the page's own JavaScript reads. They're real to the page — just invisible to that particular DevTools view.
-
DevTools' "Initiator" column shows
interceptor.jsfor every request while any rule is active on the page, because every request passes through Net Tinker's patchedfetch/XMLHttpRequestbefore reaching (or not reaching) the network. It doesn't affect behavior, only attribution. If it bothers you, addinterceptor.jsto DevTools' own Ignore List (Settings → Ignore List) — DevTools then skips those frames when computing the initiator. - A rule pinned to a tab you have since closed never fires again. A tab id means nothing once that tab is gone, so the rule stays inert forever — and reopening the same page gives you a new tab with a new id, which doesn't bring it back. You don't need the original tab: the options page shows a 📌 marker on those rules and a 📌✕ button that removes the restriction.
-
A "Modify the real response body" rule does nothing on an XHR whose
responseTypeis binary. The body can only be transformed whenresponseTypeis"","text"or"json"; with"blob","arraybuffer"or"document"there is no way to read it as text, so that particular request comes back untouched. The rule itself is fine — it just can't act there. -
"Overwrite a JSON field" leaves a non-JSON body completely alone. Same if the dot path
doesn't lead to an object (say
user.statuswhenuseris a string). The body is returned exactly as it arrived, rather than being half-rewritten or replaced by a guess. Use the "Find and replace" mode for bodies that aren't JSON. -
"Modify the real response body" skips responses that are open-ended streams.
Transforming a body means reading all of it first, and a media type whose whole contract is to stay open
—
text/event-stream(server-sent events),multipart/x-mixed-replace— never finishes. Those responses are handed back untouched, with the stream intact, and the rule simply doesn't apply to that request; the captures list records it as "matched but not applied — streaming response" so it isn't a silent no-op. The criterion is the media type, not the size: an ordinary large response does finish, so it is transformed normally — it just arrives in one piece instead of progressively. Rules of every other type leave the response body untouched and have neither effect.
Pasting a cURL to create the rule
cURL is the format everything is already in: every browser's DevTools has Copy as cURL, every API's documentation pastes examples in cURL, and whoever reports a bug sends you a cURL. In the rule form, next to Pattern, the Paste a cURL button opens a box to paste it into.
A cURL describes a request, not a response, so this fills in what the rule matches — the URL, the match mode and the method — and you write the answer. The dialog says so, and it shows you what it understood before applying anything.
-
It understands both variants Chrome copies: the bash one and the Windows
cmdone, which uses a different continuation character and different quoting. Paste whichever your system gives you. -
It works out the method: a cURL with
--dataand no-Xis a POST. -
If the command carries something that cannot be reproduced — a multipart
-F, a-d @file— it says so instead of applying half of it. - If it carries credentials (and a cURL copied from DevTools almost always does) the dialog names them. They are not stored anywhere: they don't go into the rule, and they can't be picked as a condition.
The body and the headers, if you want them. Below the summary, the dialog asks what else to use from the cURL, and by default it uses nothing:
- The body, as a condition — the rule only matches requests carrying exactly that body — or as the body to send: the rule becomes Request body with that body, for you to edit. A multi-line body can't be a condition, because that field holds a single line, and the dialog says so.
- A header as a condition, picked from the ones it carries: the rule will only match requests that send it with that same value. A rule takes one, and a cURL from DevTools carries fifteen, so you choose.
The pattern ends up as the whole URL, in Equals mode. If that URL
carries parameters that change on every call — a ?t=1725381000 — the dialog warns you: trim
them in the pattern field or the rule will never match again.
Building rules from real traffic
Open DevTools on any page and look for the "Net Tinker" panel (next to Elements, Console, Network, etc.). It lists every request the page makes while the panel is open — not just fetch/XHR, every resource type — so you can find the exact call you want to work with. The header shows Recording, and clicking it switches to Paused if you'd rather freeze the list; tick Preserve log to keep entries across navigations, and Clear empties it.
- Each row has a "+ Rule…" dropdown plus a dedicated Mock button. Picking a type opens the rule editor pre-filled with that request's real URL (as an Equals match, so it only affects that exact call) and its method. The dropdown offers every rule type except the mock, which has its own button.
- A rule built this way arrives disabled unless it's a mock or a block — those two are complete the moment they're created, while the rest are still empty skeletons, and enabling an empty "redirect" or "response body" rule would break the very endpoint you're debugging. Fill it in, then switch it on. The panel says which of the two happened each time.
- Mock goes further: it also copies the request's real status code, response headers and response body into the new rule, so you start from what the server actually returned instead of typing a fixture by hand.
- A ⚡ mark next to a row means one of your currently-enabled rules would intercept that exact request — evaluated live against your current rules, not against whatever was active when the request actually happened.
-
Each row also has a "Copy as…" option to copy the request as a
cURLcommand or afetch()snippet, for pasting into a terminal or the console. -
Export HAR saves the requests currently visible in the list as a standard
.harfile, for handing to someone else or loading into another tool. The dialog lists the sensitive values it found and offers Redact credentials (always on when the dialog opens) and Include response bodies (always off, capped at 1 MB each, because bodies are what makes the file heavy). Both boxes are reset every time it opens, so the safe option is never left off from a previous export. A HAR carries real request headers,Authorizationincluded, plus cookies — treat the file like a credential. - For a GraphQL request (detected automatically and shown as its operation name next to the URL), creating a rule from it also fills in a Body condition requiring that operation's name — because a GraphQL endpoint serves every operation at the same URL, a rule matched on URL alone would otherwise apply to all of them. You can see and edit this in the opened rule (it's just the same body condition described above, pre-filled for you). The panel's filter box also searches by operation name.
-
The panel lists everything the page does, but not everything can be mocked. Build a rule from a
row that can't be, and the panel warns you before you take it for granted: the rule is created, but it
will never fire. There are two separate reasons, and it tells you which one is yours:
-
How it was requested. An image from an
<img>, a<script src>, a stylesheet or a font never goes throughfetch/XMLHttpRequest, so there is nothing to intercept. Careful though: what counts is how it was requested, not what came back — an SVG or a PNG fetched withfetch()can be mocked, which is why they stay in the list. - Who requested it. If the site's own service worker made the call rather than the page, Net Tinker cannot reach in there — and this is the treacherous one, because the row looks exactly like an ordinary request. The panel names the script responsible so you can check it under Application → Service Workers.
-
How it was requested. An image from an
Captures shown in this panel live only in memory while DevTools is open — closing DevTools clears them, and nothing is written to storage until you explicitly turn a captured request into a rule or export a HAR. That is the difference from the captures list in the options page, which is stored on purpose.
Repeated requests, and how long everything takes
When the same request — same method, same URL — goes out several times in a row, the row carries an ×N badge. That is what turns "this feels slow" into "this is being asked for four times", which is a different conversation. The Only repeated checkbox leaves just those in the list.
The panel header also summarises the timings of what has been captured: P50, P90, P95 and P99 and the share of errors. Two things worth knowing to read it:
- It does not appear below five requests. A P95 over three samples is a number with the air of a statistic.
- Requests delayed by one of your own rules are left out, and the panel says how many. A percentile computed over calls carrying 1,000 ms you added yourself measures nothing, and looks like it does.
Searching inside the bodies
The header's search box filters by URL. The Search bodies button repeats that same search inside what travelled: useful when what you remember is not the path but an id, an error message or the name of a GraphQL operation.
Bodies are fetched from the browser one at a time and only when you ask, so over a long list it takes a moment. It is the same price you pay to look at one body, multiplied by the rows.
Copying a request to another format
Every row has Copy as…, and what it copies depends on what you want it for:
- cURL and fetch, to repeat the call from a terminal or the console.
- Playwright and Cypress, as a mock for a test.
- TypeScript types, inferred from the body that came back. It is a starting point, not a contract: what does not appear in that response cannot be guessed, and the snippet says so itself.
- Bug report, in Markdown, with the request, the response, the rule that acted if there was one, and the browser and extension versions. It goes out redacted: it is the only one on this list whose destination is a shared ticket.
Comparing two responses
The ⇄ button on a row pins it, and the one on another row opens the comparison. No request is sent: it compares two captures you already have.
JSON bodies are compared by field rather than by line, so key order produces no differences, and matches are counted instead of listed. Two controls, inside the dialog itself:
-
Ignore these fields. Write the names that change on every call by design —
timestamp,requestId,traceId,Date— and they drop out of the comparison. It matches by field name (inside lists too) or by full path, like$.data.total. It always says how many it hid: nothing is removed silently. - Mask personal data. Replaces emails, phone numbers, IBANs, cards and national ids with a marker that says what kind of value it was. It starts unchecked on every comparison, deliberately: masking too much here erases the very field you came to look at.
Tokens that appear loose inside a text (a JWT, a provider key) are always masked, regardless of that checkbox. The asymmetry is deliberate: there are no false positives to lose there, and there is a secret to leak.
Starting from an OpenAPI / Swagger spec
If the API you are mocking publishes a spec, you do not have to write the twenty rules by hand. Press
Import and pick the openapi.json (or swagger.json): every
endpoint, method and response code in it becomes a rule with its URL pattern, its status and its body
already filled in. It is the same button you use for your own export files, for a
Postman collection and for a ModHeader profile: the spec
is recognised by its own version stamp, and the ▾ next to the button lists everything it
accepts.
What is supported
- OpenAPI 3.0, OpenAPI 3.1 and Swagger 2.0. Any other version is refused by name, so you know what you are holding.
-
JSON only. If your spec is YAML, convert it first — in Swagger Editor that is
File → Convert to JSON, and from a terminal
npx js-yaml api.yaml > api.json. Reading YAML would mean bundling a YAML parser, and this extension ships no third-party code at all. Pick the.yamlanyway and it will tell you this instead of complaining about broken JSON.
Where each mock's body comes from
In this order, and it matters:
- the
examplethe spec gives for that response; - the first of its named
examples(the others are listed as not imported, by name); - the
exampleon the schema; -
a skeleton generated from the schema — the right shape with filler values
(
"string",0,true), except for formats your code actually parses (dates, UUIDs, e-mails), which get a plausible value so they don't break on arrival.
That last one is not data from the API: it is invented by this extension. Any rule carrying a generated body says so in its own description, so you can still tell weeks later. Edit it into something realistic before you trust what you see on screen.
How it is organised
- Everything lands in one group named after the API, so you can switch the whole import off with one click.
- The spec's tags become the rules' tags, so you can filter by them in the list.
- An endpoint that declares several response codes becomes a scenario with one variant per code — 200 / 404 / 500 — with the first successful one left active, so switching an endpoint between "works" and "fails" is one click. An endpoint with a single response stays an ordinary rule.
- Every imported rule arrives disabled. A spec can produce dozens at once, and turning them all on would have your app answering mocks on every endpoint before you chose any of them.
Choosing which endpoints come in
Nothing is created until you say so, and it is not all-or-nothing. A dialog first lists what the spec has — the same dialog a Postman collection gets, so the question is not answered differently depending on the file — with one row per endpoint, with its method, the URL it will match and how many rules and variants it would create — plus a filter, select all / select none over whatever the filter is showing, and a running count of what your ticks add up to. Rows come grouped by the spec's tags, in the order the spec declares them, with the tag written into the row so the filter finds it: on a 300-endpoint spec, typing a tag and pressing select all brings in that resource and nothing else.
Everything starts ticked, because importing destroys nothing — it all arrives disabled — and getting to "just this one endpoint" is three gestures away. What you do not tick is never created: the spec is walked twice, and the first pass only reads and counts. This is not "import it all and delete the rest".
What it tells you it could not do
Once you continue, the import dialog lists what
did not survive the translation, with the reason: $refs pointing at another file, schemas
that refer to themselves, oneOf alternatives where only the first was used, responses
declared as default or 4XX (there is no concrete status code to use, and picking
one would be inventing the answer), formats like image/png or text/csv for which
no body can be made up, and response headers that declare a type rather than a value. Anything that
concerns one particular response is also written into that rule's description, so it is still there once
the dialog is closed.
Re-importing a newer version of the same spec is safe: the rules are recognised as already existing (same type, method and pattern) and you choose whether to overwrite, duplicate or skip them, exactly as with any other import. A field-by-field diff that preserves your local edits does not exist yet.
What has changed since last time
When you import something you had already imported — the same spec a version later, the team's pack, a Postman collection — the dialog adds a summary of what changed against the rules you already have:
- How many are new and how many are unchanged.
-
Which ones changed shape, with the fields by name: now has
createdAt, no longer hasstatus. It compares shape rather than text, so editing the mock body by hand does not count as a change — only the API moving does. - Which ones lost their operation: rules of yours, in that group, that no longer come in the file. That is the one thing you cannot see by looking at what you are importing. Nothing is deleted: they are shown and you decide.
The extension does not need to have kept the previous spec, and it does not keep it: the comparison is between what you are bringing now and the rules in front of you.
Bringing in a Postman collection
A Postman collection goes in through the same Import button and is recognised by its own seal, just like a spec. What becomes rules is not the requests — this extension sends nothing, it answers — but the saved response examples each request carries: every one of them has a real status code, real headers and a real body, and that is a mock field by field. Unlike a spec, nothing here is generated: what you see in the rule is what the server actually answered the day the example was saved.
With no saved examples nothing comes out, and that is the common case
Saving an example is optional in Postman and plenty of people never do it, so it is entirely possible to import a two-hundred-request collection and get no rules at all. That is not a failure: there was no response to copy. The fix is in Postman, not here — open the request, send it, and use Save Response → Save as example. That stores the real response inside the collection. Repeat for every request you want to mock and export the collection again.
The importer tells you this in those words and in aggregate — how many requests came with nothing, and a few of their names — rather than repeating the same line two hundred times.
Several examples of one request become a scenario
This is what makes the fit a good one: in Postman it is common to save several named examples of the same request — "Success", "401 Unauthorized", "500" — and that is literally a scenario, one variant per example. The variant names are the ones you typed over there, so the correspondence is not invented, and the first successful (2xx) one is left active. With a single example no scenario is built: the request stays a plain rule, because a scenario with one variant is a box with one thing in it.
How it is organised
- Each saved example becomes a Mock the response (API) rule carrying the example's status, status text, headers and body.
- The collection's name becomes the one group, reused by name on re-import, so you can switch the whole import off with one click. The collection's folders become the rules' tags, which is where you can already filter.
-
The request's URL becomes a Regex pattern anchored at the end, so that
/usersdoes not also hijack the traffic of/users/42— two requests from the same collection, both imported. A path parameter (/:id) matches any value, and it says so: the collection stores the parameter's name, not what the page will send. If the URL does not come out absolute, the pattern matches that path on any host, and that is flagged too. - The query string is not part of the pattern, so the rule also fires on the same path without those parameters. It tells you which ones they were.
- Everything arrives disabled, exactly as with a spec: a collection produces dozens of rules at once, and turning them all on would have your app answering mocks on every endpoint before you chose any of them.
-
A Postman environment export — the other file, the one ending in
.postman_environment.json— goes in through the same button and produces your variables plus an environment holding their values. Importing a second environment (Dev and Prod, which is how this gets used) keeps your local value: the second file only adds its layer.
What does not come across, and why
- The request body, the authentication and the pre-request and test scripts. All three describe what a client sends, or what happens before the response arrives, and nothing here sends: it answers. They are counted in aggregate, once per collection, so they don't bury the notes that do say something different from one request to the next.
-
The example's cookies, including the
Set-Cookieheader. A response built inside the page cannot set cookies — the browser drops that header — so copying it would leave a rule promising something it cannot do. -
The headers that describe the recorded connection rather than the response:
Content-Length(it would measure the body of back then, not the one now in your rule),Content-Encoding(the example's body is stored already decompressed, so declaringgzipwould have the client try to inflate plain text),Date— the time of the recording — and the hop-by-hop ones. - An example whose body was saved as binary (an image, a PDF): the rule is created with an empty body and says so. Decoding it would put binary inside a text field.
-
An example with a status that cannot be built: a
0, which is what Postman stores when the request never answered, or a 1xx. Only200to599come in. -
A verb the rule form cannot hold (
PURGE,LINK): the rule is left on "any method" and it tells you which verb it was.
Anything concerning one particular example is also written into that rule's own description, so it is still there once you close the dialog.
Variables are resolved at import time, and the link is lost
This is the one thing about this importer you could reasonably assume works one way when it works
another, so it gets its own heading. The syntax is the same in both places
({{name}}), but a rule's pattern is never interpolated: it is
compared against real requests, so a pattern with {{baseUrl}} inside it would be a dead
rule. That is why the variables appearing in a URL are replaced by their value at import
time, using whatever value the collection carries.
The consequence, which is the part to remember: changing the variable afterwards does not move
the rules. The pattern no longer mentions it, so pointing the collection at another environment
means editing those patterns. In values — the mock's body and headers — they are kept written as
{{name}} and still interpolate on every request, like any variable here.
And two things about the names:
-
Here a variable name may only hold letters, digits and underscores. One called
api-hostis not imported — it would be a dead row in the panel, since nothing would ever substitute it — but it is used to resolve the URLs, which is what stops a known value being replaced by a wildcard. It tells you which one it was: renaming it in the file and importing again takes ten seconds, as long as you know it is needed. -
Postman's dynamic variables (
{{$randomInt}},{{$guid}},{{$timestamp}}) do not exist here and cannot: they are worth something different on every send, and this sends nothing. That part of the URL ends up matching anything, and they are counted one by one.
Which versions are accepted
- Collection v2.1 and v2.0. For what is read here they are almost the same file: the saved examples, the folders and the variables are identical.
- A v1 collection is refused by name rather than halfway through — with the way out inside the message: import it into Postman itself and export it again as Collection v2.1. That file does come in. Any other seal is refused by name too, instead of being attempted and leaving half an import behind.
- A collection that declares no format is read as v2.1 and says so. Several tools that generate collections omit that field, and assuming it in silence is the one thing that cannot be done.
Choosing which requests come in
Before anything is built you get the same dialog a spec gets: one row per request, with its method, the already-resolved URL it will match and how much it would create — its rules, and its scenario with its variants if it carries several examples — plus a filter, Select all / Select none over whatever the filter is showing, and a running count of what your ticks add up to. What you do not tick is never built.
Requests with no usable example are not in that list, because there is nothing to choose about them — but they still count in the note above, which is where their absence matters.
Re-importing a newer version of the same collection is safe: the rules are recognised as already existing (same type, method and pattern) and you choose whether to overwrite, duplicate or skip them, exactly as with any other import.
There is no journey back
A scenario cannot be exported as a Postman collection, and that is not a to-do: a rule is not a request.
Of the nine types only the mock would come close, and for "modify headers" or "block the request" Postman
simply has no concept to put them in. On top of that, a rule's pattern is a regular expression, and a
regular expression cannot be turned back into the URL it came from: an exporter would have to invent one,
and that invented URL is exactly what someone would send at a real server. To take an endpoint to another
tool there is Copy as… (cURL or fetch()), which works on the URL you are
actually looking at.
Importing a HAR capture
A HAR is traffic that actually happened: what was requested, and what the server answered, with its exact status, headers and body. You record one in the DevTools Network tab (right-click → Save all as HAR) and it comes in through the same Import button, recognised on its own.
What it is actually for, and what sets it apart from the Activity panel: traffic captured on another machine. The panel only sees what goes through this browser; a HAR arrives by email from the laptop of whoever reproduced the bug, from a customer's machine, or from a CI run. Each request becomes a mock with the answer it got that day.
What comes in and what doesn't:
- Only what this extension can touch: fetch and XMLHttpRequest. Images, styles, fonts and the document itself stay out, and the summary says how many they were.
-
One rule per method and URL, without the query. HAR URLs carry whatever they had
that day (
?_=1725354722000, a pagination token); an exact pattern against that would never match again. If you need to tell apart two calls that differ only in the query, the Pattern field is right there to put it back. - If the same request appears several times —polling, say— the last answer is kept, which is how the session ended. The list tells you how many captures there were.
- Binary responses and anything over 512 KB are dropped rather than truncated: half a JSON is broken JSON, and the failure would look like the mock's fault.
- Requests that never got an answer (aborted, failed, blocked) don't come in either: there is no response to copy.
Rules are created disabled, inside a group named after the host, so you can turn on the ones you want —or delete them all together— without the app answering yesterday's session the moment the import finishes.
Authorization headers
and tokens inside response JSON travel as they were, and the imported rules keep them — they have to,
or the mock would be useless. On import you are told how many rules carry them and of what kind. Keep
it in mind before exporting your pack and sharing it: that is where the sanitizer
lives, and where it should be ticked (what it covers and what it doesn't).
Bringing in a ModHeader profile
ModHeader left the Chrome and Edge stores in July 2026, and a lot of people were left with their exported profiles and nothing to read them. That file goes in through the same Import button and is recognised on its own: each profile becomes a Modify headers rule, which is the type that covers its whole central case. Both the array of profiles its exporter writes and a single loose profile are accepted, because its own importer accepted both.
What becomes what
- A profile becomes one rule, not one per header: twelve headers and a URL filter are conceptually a single rule. The title you gave it over there stays as the rule's name — it is what you will search for — and its short badge label becomes a tag.
- Request headers and response headers fit in the same rule, so a profile touching both is not split in two.
-
A header with a value becomes
Key: Value. A header with an empty value becomes-Key, that is, removing it: that is what ModHeader did, unless the header had its send empty header ticked. -
Its comma-separated append mode becomes
+Key: Value, which does exactly the same thing. - Its URL filter becomes the rule's pattern in Regex mode, because over there it already was a regular expression. Several filters are joined into one alternation, which is how ModHeader evaluated them: any one matching was enough. A profile with no URL filter applied to every request, and it is imported as such — with a pattern that matches everything — and said out loud.
-
Its initiator domain filter becomes the initiator condition,
anchored as a domain suffix so that
example.comdoes not also matchexample.com.attacker.net. - Its URL exclusions become Except URLs containing, whenever they can be shown to mean the same thing — see below.
- Its URL replacements become a separate Replace text in the URL rule, in the same group and sharing the pattern, method, exclusions and initiator with the headers one: a rule holds a single type, so a profile that also rewrote the URL needs two.
- The comments you had written on each header or filter go into the rule's description, each line labelled with whatever identifies its entry. That is your own text, and once the other extension is uninstalled there was nowhere to get it back from.
- Everything lands in one group called ModHeader, reused by name on re-import, so you do not end up with two of them.
Entries that were disabled in ModHeader are not brought over switched off: to its own engine an unticked header did not exist, so importing it would have invented a rule that was never there.
Everything arrives disabled, and here it matters more than elsewhere
ModHeader applied one profile at a time — the selected one, plus any you had on always on. Switching on everything imported at once would assemble header combinations you never had together; and a profile that did not filter by URL becomes a rule that matches everything. Both of those are exactly "breaking the site you are debugging". The profiles that were on always on are flagged at import as the first candidates to enable, which is the information that separates "I want this one live now" from the rest of the batch.
What does not convert, and why
Everything below is reported at import, profile by profile and with its reason:
-
An exclusion that is a real regular expression. Exclusions here are
literal text, so only the ones that can be shown to be equivalent come across: a
pattern with no loose metacharacters, and the usual
.*/something.*shape, which is exactly "contains/something". The rest stay out with their note — and this is the only loss in this importer that makes the rule fire on more than it should rather than less, so it is the one to review first. -
A URL replacement that is a regex, or that uses capture groups (
$1): the text searched for here is literal, so there would be no groups to reference and a$1would be written raw into the URL. - Its append mode with no separator at all. Here appending uses a comma, so it is converted to the closest thing and it tells you which header — that keeps more than dropping the header outright.
- A method restriction that does not fit into a rule's single Method: it is left on "any" and reported.
- What has no equivalent here: its cookie headers, its CSP headers, and its filters by tab, tab group, window, resource type and time of day.
- Any field a later version of ModHeader added that this one does not know. It says which field it was, which is what you need to fix it by hand instead of discovering it by its absence.
A profile from which nothing can be converted — because it only held things from that list — produces no rule at all, and that is reported too.
Nothing is sanitized on import, unlike on export: an
exported file gets shared, but an imported one is your own data coming back to your own machine.
Redacting an Authorization here would protect nobody and would leave a rule that does not
work, with nowhere to recover the value from.
What "sanitize on export" does (and doesn't) cover
Export writes your rules and groups to a JSON file, together with your variables and environments. Exporting everything includes every variable you have; exporting a selection of rules only includes the variables those rules actually reference by name, and prunes each environment down to the same set. Which environment is currently active is a local preference and is never part of the file.
Because that file gets shared, the export goes through a dialog first. It lists what it found that looks
like a credential, and ticks Replace credentials with __REDACTED__ for you
when there is anything to hide. Sanitizing looks for names that suggest a credential — anything
containing auth, token, secret, password,
passwd, apikey, api_key, api-key,
credential, signature, cookie or session — and
replaces the matching value with __REDACTED__. It checks:
- Header names, both in a mock's headers and in the request/response header operations of a "Modify headers" rule.
- Query parameter names of a "Query parameter" rule.
- Field names anywhere inside a JSON body, at any depth.
-
The query string of the rule's own URL pattern and of a redirect URL — but only the query string. A path
segment could be anything, so guessing there would mangle legitimate URLs. A pattern whose match type is
Regex is skipped entirely, because splitting a regex on
?would corrupt it. -
Variable names —
{{token}},{{apiKey}}and friends are the likeliest secret in the whole file. An environment's overrides are judged by the name of the variable they point at, so a "Prod" override oftokenis redacted exactly like the variable itself.
It deliberately does not touch:
- Non-JSON request/response bodies (plain text, XML, form-encoded, etc.) — there's no reliable way to tell a token apart from ordinary text in an arbitrary format, and guessing wrong would silently corrupt the body. Only bodies that parse as JSON are inspected, and the dialog names the bodies it could not check so you can review them yourself.
-
A value under a field name that doesn't look sensitive — a bearer token stored under a field literally
named
valueordatawould not be caught, because the check is name-based, not content-based. -
Booleans, or the literal strings
"true"/"false", even under a sensitive-looking name. A flag is not a secret, and redacting it would just produce a broken rule. -
Two header names that look sensitive but never carry one, whatever their value:
access-control-allow-credentials(a fixed CORS flag) andwww-authenticate(the challenge that makes a mocked 401 realistic). - The fields of a "Modify the real response body" rule — the text you search for and replace, and the JSON field value you write. Those are exported as you typed them, so don't park a token in there before sharing a file.
- The rule's own name, description and tags — those are yours to write, so nothing there is scanned.
Treat sanitize-on-export as a safety net against the most common accidental leak, not as a guarantee that an exported file contains no secrets — review what you're sharing before you send it.
On the way back in, Import lets you pick what to bring over and warns you when a rule already exists (same type, method and pattern), offering to import it as a copy, skip it or overwrite the existing one — with the same choice, separately, for variables whose value differs from yours. It is the same path an OpenAPI spec, a Postman collection and a ModHeader profile take, which is why none of them has an import mechanism of its own.