Showing Permission Status on the Options Page
Show users which permissions and site access an MV3 extension has: listing granted and optional permissions with chrome.permissions, requesting and removing them from options, explaining host access, and reacting to changes made elsewhere.
Table of Contents
A user restricts the extension to “On click” in Chrome’s site access menu, forgets they did, and a week later reports that highlighting has stopped working. Another user granted an optional permission for one feature and now wants to take it back, but cannot find where. Permissions are part of an extension’s state that users can change outside the extension’s own UI — through the toolbar’s site access menu, chrome://extensions, or enterprise policy — and the extension behaves differently as a result. An options page section that shows what is granted, explains what each permission does, and lets users grant or revoke optional ones turns a confusing failure into a self-service fix. This guide builds that section. It belongs to options page layouts.
What can be shown
chrome.permissions.getAll() returns the extension’s currently granted API permissions and host origins — required ones from the manifest plus any optional ones the user has granted. The manifest itself (chrome.runtime.getManifest()) lists what is optional (optional_permissions, optional_host_permissions). chrome.permissions.contains() checks a specific set. Users can withhold host permissions that were declared as required (the “On click” or “On specific sites” setting), so a required host permission may not actually be granted; getAll reflects the effective state. permissions.onAdded and onRemoved report changes, including those made from browser UI. Optional permissions can be requested with permissions.request() from a user gesture and removed with permissions.remove().
Step-by-step: a permissions section
1. Describe each permission in user terms
1// permission-copy.js
2export const PERMISSION_INFO = {
3 storage: { feature: "permStorageFeature", optional: false }, // "Saves your settings"
4 contextMenus: { feature: "permMenusFeature", optional: false },
5 downloads: { feature: "permDownloadsFeature", optional: true }, // "Export your library as a file"
6 notifications: { feature: "permNotifyFeature", optional: true }, // "Reminders for saved pages"
7 history: { feature: "permHistoryFeature", optional: true }, // "Suggest pages you visited often"
8};
Execution context: a shared module. Users care about features, not API names. Map each permission to the feature it enables, with localised copy, and mark which are optional. This copy also helps with store review — the same explanations belong in your listing and privacy policy, as in reducing permission warnings at install.
2. Read the effective state
1export async function permissionState() {
2 const granted = await chrome.permissions.getAll();
3 const m = chrome.runtime.getManifest();
4 const optionalApis = m.optional_permissions ?? [];
5 const requiredHosts = [...(m.host_permissions ?? []), ...(m.content_scripts ?? []).flatMap((c) => c.matches)];
6 const optionalHosts = m.optional_host_permissions ?? [];
7 return {
8 apis: [...new Set([...(m.permissions ?? []), ...optionalApis])].map((p) => ({
9 name: p, granted: granted.permissions.includes(p), optional: optionalApis.includes(p),
10 })),
11 hostsGranted: granted.origins,
12 hostsWithheld: requiredHosts.filter((h) => !granted.origins.includes(h)),
13 optionalHosts,
14 };
15}
Execution context: the options page. Comparing the manifest with getAll() reveals required host access the user has withheld — the most common cause of “it stopped working” — and optional permissions not yet granted. Content script matches count as host access too.
3. Render rows with actions
1async function render() {
2 const state = await permissionState();
3 const list = document.querySelector("#permissions");
4 list.replaceChildren(...state.apis.map(({ name, granted, optional }) => {
5 const info = PERMISSION_INFO[name];
6 const row = document.createElement("li");
7 row.innerHTML = `<span class="feature"></span><span class="status"></span>`;
8 row.querySelector(".feature").textContent = chrome.i18n.getMessage(info?.feature ?? "permOther", [name]);
9 row.querySelector(".status").textContent = chrome.i18n.getMessage(granted ? "permOn" : "permOff");
10 if (optional) {
11 const btn = document.createElement("button");
12 btn.textContent = chrome.i18n.getMessage(granted ? "permRemove" : "permEnable");
13 btn.addEventListener("click", () => toggle(name, granted));
14 row.append(btn);
15 }
16 return row;
17 }));
18 renderHostAccess(state);
19}
Execution context: the options page. Required permissions are shown with explanations but no button — they cannot be removed without uninstalling. Optional ones get Enable or Remove. Text is set with textContent; the static innerHTML contains no data.
4. Request and remove from a user gesture
1async function toggle(name, granted) {
2 try {
3 const ok = granted
4 ? await chrome.permissions.remove({ permissions: [name] })
5 : await chrome.permissions.request({ permissions: [name] });
6 if (!ok && !granted) showNote(chrome.i18n.getMessage("permDeclined"));
7 } catch (e) {
8 showNote(chrome.i18n.getMessage("permUnavailable")); // e.g. blocked by enterprise policy
9 }
10}
Execution context: the options page, inside a click handler. permissions.request must be called directly from a user gesture, before any other await, or it fails. It shows the browser’s permission prompt and resolves false if the user declines. Removing an optional permission needs no prompt. Errors usually mean the permission is blocked by policy; explain rather than retry. See requesting optional permissions at runtime.
5. Explain and fix withheld host access
1function renderHostAccess(state) {
2 const box = document.querySelector("#site-access");
3 if (!state.hostsWithheld.length) {
4 box.textContent = chrome.i18n.getMessage("siteAccessAll"); // "Works on all sites"
5 return;
6 }
7 box.textContent = chrome.i18n.getMessage("siteAccessLimited"); // "Highlighting only works on sites you allow"
8 const btn = document.createElement("button");
9 btn.textContent = chrome.i18n.getMessage("grantSiteAccess");
10 btn.addEventListener("click", () => chrome.permissions.request({ origins: state.hostsWithheld }));
11 box.append(btn);
12}
Execution context: the options page. Users who chose “On click” did so deliberately, so explain the consequence neutrally and offer the fix rather than nagging. In Chrome, required host permissions that were withheld can be re-requested with permissions.request. See handling user-restricted site access.
6. Refresh when permissions change anywhere
1chrome.permissions.onAdded.addListener(render);
2chrome.permissions.onRemoved.addListener(render);
3document.addEventListener("visibilitychange", () => { if (!document.hidden) render(); });
Execution context: the options page. Permission changes from the toolbar site access menu or chrome://extensions fire these events; re-rendering on visibility covers any edge cases. See detecting when host permissions are granted or revoked.
7. Link to the browser’s controls
Add a “Manage in browser settings” link that opens chrome://extensions/?id=<id> with chrome.tabs.create (using chrome.runtime.id). Some controls — incognito access, file URL access, “pin to toolbar” — can only be changed there, and chrome.extension.isAllowedIncognitoAccess() / isAllowedFileSchemeAccess() let you show their current state.
Common mistakes
- Showing API names instead of features. Users don’t know what
historyenables. - Calling
requestafter an await. It fails without a direct user gesture. - Assuming required hosts are granted. Users can withhold them.
- No refresh on external changes. The page shows stale state.
- Remove buttons for required permissions. They cannot be removed.
Cross-browser variation
- Chrome / Edge: users can withhold required host permissions;
permissions.requestcan re-grant them. - Firefox: in MV3 all host permissions are optional and not granted at install, so the “withheld” state is the default — this section is essential there. See host permissions in Firefox MV3 are optional.
- Safari: users grant website access per site in Safari settings;
permissions.getAllreflects it, and requests show Safari’s prompt.
Verification
- Set site access to “On click” and confirm the page explains it and offers Grant access.
- Grant an optional permission from options and confirm the row updates.
- Remove it from
chrome://extensionsand confirm the page updates without reload. - Check that required permissions have explanations but no Remove button.
FAQ
Can I remove a required permission?
No. Only optional permissions can be removed; required ones go away only with the extension.
Why does request() resolve false without a prompt?
Either the user already declined in this gesture, the call was not in a user gesture, or policy blocks it.
Should features check permissions before running?
Yes. Use permissions.contains and show a prompt to enable rather than failing silently.
Is it worth showing required permissions at all?
Yes. A short explanation of why each one is needed builds trust, especially for permissions with install warnings, and answers the question users otherwise ask in reviews.
Can the page show which sites the user allowed under “On specific sites”?
Yes. Those origins appear in getAll().origins; list them with a Remove button that calls permissions.remove({ origins: [origin] }).
Related
- Handling user-restricted site access — withheld hosts.
- Detecting when host permissions are granted or revoked — the events.
- Reducing permission warnings at install — optional permissions.
- Options page layouts — the parent topic.