What happens when the user taps Block? A permission-refusal axis proposal (September 2026)
Everyone who tests a camera or location feature presses Allow, and the browser then stops asking. This proposal scores the path nobody sees: what the app does after somebody taps Block, and whether it can tell that refusal from the header, the scheme or the missing gesture that share its name.
Updated on September 27, 2026
On this page
Quick Answer
When a browser refuses a powerful feature, it refuses in a way the application cannot easily read. The one API built to report permission state collapses a user's choice, a response header, an insecure page and a missing user gesture into the single string denied. The camera API names one error for five different causes. The geolocation error constant called PERMISSION_DENIED is documented with a policy header as its example. So the question this axis asks is not whether a generated app requests permissions politely. It is what the app does in the seconds after somebody taps Block, whether it can tell that refusal apart from the other things that produce the same signal, and whether the way back it offers is a way back at all. We are proposing the rubric before we score anything against it, in September 2026.
Why refusal deserves its own axis
Every review of a generated application that touches a camera, a location, a microphone or a notification is performed by somebody who wants that feature to work. They click Allow. The browser records the answer against the origin and stops asking. From that moment the refusal path is not merely untested, it is unreachable without a deliberate trip into browser settings, and nothing in a build loop ever takes that trip.
That is the whole problem, and it is worth being precise about why it is not simply carelessness. The web platform provides no way for a page to put the browser back in the state it needs in order to test this. The Permissions API documentation is explicit:
MDN, Permissions API, verbatim: "Permission revocation is not managed by the API. More specifically, a Permissions.revoke() method was proposed, but has since been removed from those browsers where it was implemented."
The same page then gives manual click paths through the Firefox and Chrome settings screens, which is the platform conceding that the only reset is out of band, performed by a human, in a surface the application cannot reach. An application cannot re-ask. It cannot revoke. It cannot restore the condition under which its own refusal path would run.
A benchmark axis is worth proposing when a defect is common, consequential and invisible to the person best placed to find it. This one qualifies on all three.
What permission-refusal posture means here
It means the observable behaviour of the generated application when a powerful feature is not available to it, across the distinct refusals that produce that outcome. It does not mean access control. A row-level security policy deciding that this account may not read that record is a different subject, owned by a different axis, and the word permission is doing different work in the two sentences.
The W3C specification draws the line we are using:
W3C Permissions, section 3.3, verbatim: "A powerful feature is a web platform feature (usually an API) for which a user gives express permission before the feature can be used. Except for a few notable exceptions (e.g., the Notifications API Standard), most powerful features are also policy-controlled features."
Two layers, in the specification's own words. A user answer, and a document policy. The axis is about what the application does when either one says no.
Three APIs, three collapses, one word
The finding that makes this measurable is that the platform refuses informatively to the browser and uninformatively to the page. We read three independent surfaces and all three collapse distinct causes into a single signal.
The Permissions API aggregates four refusals into one value
MDN, Permissions API, verbatim: "The permissions from this API effectively aggregate all security restrictions for the context, including any requirement for an API to be used in a secure context, Permissions-Policy restrictions applied to the document, requirements for user interaction, and user prompts. So, for example, if an API is restricted by permissions policy, the returned permission would be denied and the user would not be prompted for access."
Read the consequence. A page served over plain HTTP, a page whose response carried a restrictive Permissions-Policy, a page that called the feature without a user gesture, and a page whose user pressed Block all read back the same string. Four causes, four completely different remedies, one value. Moving to HTTPS, editing a response header, moving a call into a click handler, and writing instructions for a settings screen are not interchangeable repairs, and the API that exists to tell you which one you need does not.
The camera API names one error for five causes
MDN, MediaDevices.getUserMedia(), on NotAllowedError, verbatim: "This will happen if the browsing context is insecure (that is, the page was loaded using HTTP rather than HTTPS). It also happens if the user has specified that the current browsing instance is not permitted access to the device, the user has denied access for the current session, or the user has denied all access to user media devices globally. On browsers that support managing media permissions with Permissions Policy, this error is returned if Permissions Policy is not configured to allow access to the input source(s)."
The sentence is worth reading twice, because it names three separate scopes of user refusal inside one error: this browsing instance, this session, and globally. Those have different lifetimes. A session refusal is gone after a reload. A global refusal is not. The application receives an identical NotAllowedError for both and therefore cannot know whether asking again is a reasonable thing to offer or a button that will do nothing forever.
The geolocation constant is documented with the policy as its example
MDN, GeolocationPositionError, code 1, verbatim: "The acquisition of the geolocation information failed because the page didn't have the necessary permissions, for example because it is blocked by a Permissions Policy."
The constant is literally named PERMISSION_DENIED, and the example MDN reaches for is not a user at all. It is a header. And the field that might disambiguate is ruled out on the same page: "Specifications note that this is primarily intended for debugging use and not to be shown directly in a user interface." A message you are told not to show, attached to a code that cannot tell you who said no.
The honest counterweight
None of this is a platform failure. Refusing to tell a page why it was refused is a deliberate and correct privacy decision, because a page that could distinguish "this user blocks cameras everywhere" from "this policy disallows it here" learns something about the person. The collapse is a feature. What the axis measures is not whether the platform should be more forthcoming. It is whether the application behaves sensibly given that it will never find out.
The lifetime of an answer is not specified
The specification declines to pin how long an answer lasts, and says so plainly:
W3C Permissions, verbatim: "For other features, like the Geolocation, user agents are known to offer a choice of only granting the permission for the session, or for one day. Others, like the Notifications API Standard and Push API APIs, remember a user's decision indefinitely or until the user manually revokes the permission. Note that permission lifetimes can vary significantly between user agents."
This is the kind of stated decline that makes an axis worth proposing rather than an argument worth having. The specification is not silent on lifetime, it explicitly hands it to implementers and warns that it varies. So an application cannot reason about permanence from the specification either, and any interface copy that tells a user what will happen next is making a claim the platform does not support.
The retry that cannot work
The strongest single fact here is normative rather than descriptive. The specification's algorithm for requesting permission begins:
W3C Permissions, section 5.2, verbatim: "Let current state be the descriptor's permission state. If current state is not prompt, return current state and abort these steps."
Step two. If the state is already denied, the request returns denied immediately and the user is never asked. This is not a browser quirk and not an anti-abuse heuristic layered on top. It is the specified behaviour. A retry button wired to call the feature again will therefore resolve instantly with the same refusal, show no prompt, and change nothing, every time, forever.
That matters for a reason beyond this axis, and we will come back to it below, because one of our own rubrics rewards exactly that button.
Three states, not two
There is a third answer, and it is the one most implementations lose. The notification permission is documented with three values:
MDN, Notification.requestPermission(), verbatim: "granted The user has explicitly granted permission for the current origin to display system notifications. denied The user has explicitly denied permission for the current origin to display system notifications. default The user decision is unknown; in this case the application will act as if permission was denied."
A dismissal is not a denial. A user who presses Escape, or clicks elsewhere, has not answered, and the browser will ask again on a later attempt. A user who pressed Block has answered and the browser will not. Those two futures are opposite, and the recommended behaviour is to treat them identically at the moment of failure. An application that conflates them will either badger someone who said no, or give up permanently on someone who said nothing.
The proposed rubric
Seven signals, one hundred points. Each row states the failing case, because a failing case is something a reader can check against their own build this afternoon.
Scroll to see more
| Signal | Weight | What a failing case looks like |
|---|---|---|
| A refusal reaches the interface at all | 22 | The promise rejects, nothing catches it, and the view sits on a spinner or an empty region indefinitely; the only evidence anything happened is a console entry the user will never open |
| Refusal is separated from the causes that share its name | 20 | Every NotAllowedError is rendered as the same message, so a build served over plain HTTP, a build denied by its own response header and a build whose user pressed Block all tell that user the same wrong thing |
| A dismissal is not treated as a denial | 16 | The code branches on granted against everything else, so default and denied take one path; the person who pressed Escape is never asked again, or the person who pressed Block is asked on every visit |
| The way forward matches what the platform can do | 14 | A Try again control is offered after a denial, calling the same method, which returns the stored refusal without showing a prompt; the button is inert by specification and the interface does not say so |
| Revocation while the document is open is observed | 12 | The permission is read once at mount and cached; a user who revokes it in settings while the tab stays open leaves the application acting as though it still holds a capability it lost |
| Policy-controlled features are declared deliberately | 9 | No Permissions-Policy is sent, so each directive falls back to its own default allowlist and the effective policy is whatever each engine happens to choose; or one is sent that disables a feature the application itself uses |
| The refusal path is exercised rather than assumed | 7 | No test, fixture or documented manual step ever runs the feature against a denial, so the branch ships unexecuted and the first person to run it is a user |
Three weighting decisions worth arguing about now
Why reaching the interface carries the most. It is the only row whose failure produces no information at all. Every other row describes an application that says something imperfect. This one describes an application that says nothing, and a permanent spinner is indistinguishable to the user from a broken product.
Why distinguishing causes outranks the recovery affordance. It has to, because the affordance depends on it. You cannot offer a correct way forward until you know whether the obstacle is the user, the header or the scheme, and offering the wrong one is worse than offering none. Signal 4 is downstream of signal 2, and weighting them the other way round would reward a confident wrong button.
Why the policy declaration is small. It is real, and it is also the row a generator has least to do with, since a missing header is a default rather than a decision. Weighting it heavily would measure the hosting platform more than the build.
Four postures
Level 0, Unhandled. The refusal produces a rejected promise nothing acts on. The view does not change. There is no message, no fallback and no record.
Level 1, Announced. Something visible happens. It is the generic failure state, it does not name the cause, and any recovery control it offers calls the same method again.
Level 2, Distinguished. The application separates a refusal from the transport and policy causes that share its error name, and separates a dismissal from a denial. What it tells the user is true in each case.
Level 3, Fallback-complete. Everything in Level 2, plus the feature degrades to a path that does not require the capability, a revocation occurring while the document is open is observed and acted on, and the policy layer is declared rather than inherited.
The ladder is uneven and it is worth saying so. Level 0 to Level 1 is a catch block. Level 1 to Level 2 needs the developer to have read what their error name actually covers. Level 2 to Level 3 needs a second implementation of the feature, and that is a product decision rather than a code quality one. We would rather publish an uneven ladder than pretend the rungs cost the same.
How to reproduce it
- Prompt the builder for a feature that needs a powerful feature, described only as a feature. Do not mention permissions, denial or fallbacks. Anything the generator does about refusal has to be unprompted or it is measuring the prompt.
- Census the export for every powerful feature it calls. Record zero as zero; a build that touches none of them is a valid observation and not a failed run.
- Before the first run, open browser settings and reset site permissions for the origin. Everything below depends on this step, and it is the step that cannot be performed from the page.
- Control, which carries no information. Run the feature once and press Allow. Record that it works. A Level 0 build and a Level 3 build pass this step identically, which is exactly why it is recorded as a control and never as a result.
- Reset again. Run and press Block. Watch the interface for sixty seconds without touching it, and write down what a person who does not read consoles would conclude.
- Reset again. Run and dismiss the prompt without answering, by pressing Escape or clicking away. Compare against step 5. If they are indistinguishable, signal 3 fails.
- Without resetting after step 5, reload and trigger the feature again. Does a prompt appear? If the interface offers a retry, press it and time how long the refusal takes to come back. An instant refusal with no prompt is the specified behaviour, not a bug.
- Query the state directly, with
navigator.permissions.query({ name: "geolocation" })or the equivalent for the feature under test, and record the returned string alongside what the interface was showing at the same moment. - Serve the same build with a response header disabling the feature, for example
Permissions-Policy: geolocation=(), grant nothing, and compare the error the application receives against the one from step 5. This is the discriminator for signal 2, and it is the step that makes the collapse concrete rather than quoted. - Grant the permission, leave the document open, revoke it in browser settings, and watch whether anything in the interface reacts.
The named trap: the first-answer illusion
Every axis we propose names the illusion that hides its defect. This one is the first answer you ever gave.
The first time a developer meets their own permission prompt, they are testing a feature they have just built and they want to see it work, so they press Allow. The browser stores that answer against the origin. From then on the prompt does not appear, the refusal branch does not execute, and the developer's experience of their own application is permanently the happy path. The condition required to see the defect was consumed by the most natural action available, at the earliest possible moment, by the only person looking.
It belongs to a family we keep finding on this property, where the only evidence available at test time is generated by a single actor. What makes this one distinctive is that the actor cannot undo it from inside the work. A page cannot revoke. A page cannot re-prompt. The reset lives in a settings screen, and reaching it requires leaving the loop entirely and going to fix a browser rather than an application, which is not a thing a build session ever prompts anybody to do.
There is a sharper version on a generated-application platform specifically, and we flag it as a mechanism rather than a measurement because we have not run it. Permissions are stored against an origin. A preview surface that serves many projects from one origin would therefore carry one permission store across all of them, and an answer given while testing one project would apply to the next project on first load. Under that arrangement a developer could ship an application whose permission prompt they have never seen, because they answered it months ago on behalf of something else. Whether any particular platform is arranged that way is an empirical question, and we have not measured it.
Why this is not already covered
It is not browser and engine compatibility. That axis asks whether the engine can run what was shipped. This one assumes it can. The distinction is checkable rather than rhetorical: geolocation has been Baseline Widely available since July 2015 and the Permissions API since September 2022, so the instruments that axis uses, a Baseline floor and a resolved target list, report these features as universally present, which they are. Presence is not authorisation. A build can hold a perfect score there and be exactly as exposed to a Block as a build with no floor at all. The reverse holds too: a feature can be permitted in principle and simply absent from the engine. One genuine overlap should be stated rather than argued away, because Notification.requestPermission() is marked Limited availability and is explicitly not Baseline, so for notifications specifically that axis does have a real claim and ours is not the only one.
It is not security headers and response hardening, and there is a genuine tension. That rubric's seven signals cover cookie attributes, script execution, embedding, delivery, transport pinning, content type and referrer, and policy coherence. Permissions-Policy is not among them, and the boundary is clean in principle: that axis asks what the server declares in order to constrain an attacker, this one asks what the application does when a capability is refused. The tension is that the header is one of the things that can do the refusing. A default-deny hardening pass is ordinary good practice, and applied to a feature the application actually uses it produces a refusal indistinguishable, at the point of failure, from a user pressing Block. So a build can improve its score there and regress here, through one line, with no error anyone would notice. We are not proposing that anyone stop sending the header. The resolution is ordering: declare the policy deliberately, which is signal 6, and then make the refusal legible, which is signal 2. There is also an asymmetry worth recording, which is that the header is itself marked Limited availability and Experimental while the capabilities it governs are Widely available, so a non-Baseline mechanism is governing Baseline features and the effective policy where it is unsupported is each directive's own default allowlist.
It is not state-handling completeness, and this is the sharpest boundary in the set. That axis asks whether a view renders something sensible while loading, when empty and when a request fails, and it is right to ask. A refusal is none of those three. It is not a failure to be retried, it is a decision that has been made and stored, and the interface's correct response is to stop asking rather than to recover. The tension is concrete and it sits in one row. That rubric awards its top recovery score for "Retry, refresh, or fallback offered inside the state", and by section 5.2 of the Permissions specification a retry after a denial returns the stored answer without prompting. So one of our rubrics rewards an affordance that this one identifies as inert. We do not think that row is wrong; on a network failure the retry is exactly right. The resolution is classification, which is what signal 2 exists to carry: the same control is correct for a transport failure and wrong for a stored refusal, and an application that cannot tell them apart cannot choose correctly. It is worth noting that this is the second of our own rubrics to collide with that single recovery row, after the duplicate-write problem our outbound call failure axis records against it, which suggests the row wants a condition rather than a higher weight.
It is not consent gating. Our consent axis asks whether the application collects before it has permission to collect, which is a question about our own code, our own banner and a legal obligation we can discharge. This asks whether the user agent will hand over a capability at all, which is not ours to store, clear or re-ask. Both directions hold. A build can be scrupulously consent-gated and still hang forever on a denied camera, and a build can handle every refusal elegantly while running trackers before anyone agrees to anything.
It composes with version skew, and the composition is not a boundary. That axis is about a document that stays open while the server moves past it. Signal 5 here is the same shape on a different subject: a document that stays open while its own permissions move past it, because a user revoked one in a settings screen. The specification carries a section named Reacting to users revoking permission, and the mechanism is a change event on the permission status object rather than anything the document would discover by asking again. Saying that the two rhyme is more useful than drawing a line that is not there. There is also a composition with client network failure that we will name without stretching it: MDN's own list of causes for the fetch failure type already includes a request blocked by a permissions policy, so that axis and this one meet at the same header from opposite sides.
Why we are naming a pattern and not publishing placements
Because we have not run it. We have read specifications and vendor documentation and no generated application. Every posture above is a structure, not a result. No builder is placed on this axis, no level is assigned to any product, and nothing here should be read as a measurement of anybody. Our output-quality set and its composite were withdrawn in August 2026 and have not been reinstated, and publishing a placement from a rubric that has never produced a number would repeat precisely the mistake that withdrawal was for.
Limitations and open questions
- Documentation, not applications. The largest gap on this page, and a deliberate scope choice for an axis proposal.
- Step 3 is manual and not identical between testers. Resetting site permissions is a browser settings operation with a different path per browser and per version, and MDN's own instructions for one of them describe a settings layout that has since changed. Two people performing this protocol will not perform the same reset, and until that is pinned the result belongs beside the score rather than inside it.
- Signal 3 may not be measurable from inside the page. The Permissions API reports
promptboth for a feature never asked about and, on at least some engines, for one where a prompt was dismissed. If those are genuinely indistinguishable to a page, then the dismissal signal can only be scored by observing the interface across the protocol's steps 5 and 6 rather than by reading a state, and a rubric row that cannot be read programmatically is weaker than one that can. - Signal 1 could punish the better design, so it does not. A build that needs no powerful feature has no refusal path to get wrong. That scores a full pass on signal 1 rather than a blank, because the axis is about handling what you reach for, and reaching for nothing is a legitimate answer to it.
- Notification is a real exception to our own carve-out. It is not Baseline, so the browser-support axis has an overlapping claim on it, and the boundary we drew above holds for geolocation, camera and microphone more cleanly than it does for notifications.
- Signal 5 may be over-weighted at 12. Revocation while a document stays open is a genuine failure and it is also rare, and we are not confident that it deserves more than the policy declaration row.
- The shared-origin preview mechanism is unmeasured. It is stated above as a mechanism precisely because we have not tested it against any platform, and it should not be repeated as a finding until somebody has.
Counter-rubrics, corrections and reproduction attempts are welcome. The most useful thing anyone can send is the output of steps 5, 6 and 9 against a project of their own, with the browser and version recorded, because that triple is what separates the three refusals this axis is built around.
References
- W3C Permissions specification, editor's draft, read 27 September 2026: sections 3.3 on powerful features, 5.2 on requesting permission, 5.4 on reacting to revocation, and the non-normative usage example.
- MDN Web Docs, Permissions API, read 27 September 2026.
- MDN Web Docs,
MediaDevices.getUserMedia(), read 27 September 2026. - MDN Web Docs,
Notification.requestPermission()static method, read 27 September 2026. - MDN Web Docs,
GeolocationPositionError, read 27 September 2026. - MDN Web Docs,
Permissions-PolicyHTTP response header, read 27 September 2026.
Written by
BuilderProof editorial teamCite this benchmark
BuilderProof editorial team. "What happens when the user taps Block? A permission-refusal axis proposal (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/what-happens-when-the-user-taps-block-permission-refusal-axis-september-2026.
@misc{builderproof-what-happens-when-the-user-taps-block-permission-refusal-axis-september-2026,
title = {{What happens when the user taps Block? A permission-refusal axis proposal (September 2026)}},
author = {{BuilderProof editorial team}},
year = {2026},
month = {sep},
howpublished = {\url{https://www.builderproof.org/benchmarks/what-happens-when-the-user-taps-block-permission-refusal-axis-september-2026}},
note = {BuilderProof, builderproof.org}
}Frequently asked questions
What is permission-refusal posture?
It is the observable behaviour of a generated application when the browser will not hand it a powerful feature such as camera, microphone, geolocation or notifications. It covers whether a refusal reaches the interface at all, whether the application can separate a user's refusal from the other causes that produce the same error, and whether the way forward it offers is one the platform can actually deliver. It is not access control: a rule deciding which account may read which record is a different subject.
Why can an application not simply ask again after a denial?
Because the specification forbids it. Section 5.2 of the W3C Permissions specification defines the request algorithm as: let current state be the descriptor's permission state, and if current state is not prompt, return current state and abort these steps. Once the stored state is denied, a request resolves immediately with denied and the user is never shown a prompt. A Try again control wired to call the feature again is therefore inert by specification rather than by browser quirk.
Why is a denied permission hard to tell apart from other failures?
Because the platform deliberately collapses the causes. MDN states that the Permissions API aggregates secure-context requirements, Permissions-Policy restrictions, user-interaction requirements and user prompts into one value, so a page restricted by policy reads back denied without the user ever being asked. The camera API names a single NotAllowedError for an insecure context, three different scopes of user refusal and a policy block. The geolocation code named PERMISSION_DENIED is documented with a Permissions Policy block as its example. The collapse is a privacy feature, not a defect, which is why the axis measures the application's response rather than asking the platform to disclose more.
What is the difference between a dismissal and a denial?
A denial is an answer the browser stores and will not ask about again. A dismissal is no answer at all, and the browser may ask again later. MDN documents three notification permission values, granted, denied and default, and describes default as the case where the user decision is unknown and the application will act as if permission was denied. Treating the two identically produces one of two wrong outcomes: badgering somebody who said no, or permanently giving up on somebody who said nothing.
Has BuilderProof scored any builder on this axis?
No. This is a pre-registration published before any measurement exists. Every posture described is a structure rather than a result, no product is placed, and no level is assigned to any builder. The rubric, the weights and the protocol are published first so that they can be argued with before any number is produced.
Related benchmarks
Does It Run in the Browser They Actually Have? A Proposed Browser-Support Axis for AI App Builder Output (September 2026)
A proposed benchmark axis for which browser engines a generated app can actually run in. Baseline defines widely available as 30 months of interoperability and names what it cannot see. Next.js floors at Firefox 111, Vite resolves its default to Firefox 114, and the preview pane reports a pass either way. Seven weighted signals, four postures, a ten-step protocol, and no scores.
What security headers does your generated app actually send? A response-hardening axis for AI app builders (September 2026)
A proposed BuilderProof axis measuring whether the applications AI app builders generate actually deliver security response headers, and whether the policies they do ship bind where they are delivered. Seven weighted signals, four postures, a ten-step protocol with a control step. Pre-registered: no builder is scored here.
State-handling completeness: a proposed benchmark axis for AI app builders (August 2026)
State-handling completeness is a proposed BuilderProof benchmark axis (August 2026) that scores how well an AI app builder generates the non-ideal runtime states of the apps it produces: loading, empty, and error states. It is a 20-point axis across five sub-criteria, measured reproducibly by giving all five commercial builders (v0, Lovable, Replit, Base44, Bolt.new) an identical fixed prompt and then inspecting the generated app under a throttled network, an empty account, and a forced request failure.