> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Install the Context Firewall extension

> Install the VisIQ Context Firewall browser extension — by hand for a single machine, or fleet-wide through your existing managed-browser policy.

<Note>
  **The supply routes are not equally ready. Read this table before you plan a rollout.** Every claim below was measured against the live system, on the date given in its row, not inferred.

  | Route                                          | State today                                                                                                                                                                                                                                                                                                                                                                                                                                              |
  | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | **Chrome / Edge: Chrome Web Store** (unlisted) | **Usable now.** The listing is published as *unlisted*: reachable through the link your VisIQ team gives you, not by searching the store. It served **0.1.1** when the store was last checked, on **2026-09-20**.                                                                                                                                                                                                                                        |
  | **Chrome / Edge: self-hosted CRX**             | **Usable, and the recommended route for a managed fleet.** The update manifest served **0.1.11** over `https` when checked on **2026-09-21**, and a real `ExtensionInstallForcelist` policy naming `fmjgcgdgkiednbkkjkgcphblopgenjjn` was proven by execution to force-install it on **Google Chrome 153** and on **Microsoft Edge 153**, with the store copy blocked by the same profile. No store review sits between a published fix and your estate. |
  | **Safari on macOS**                            | **Installer is published and serving.** `GET /firewall/dist/visiq-context-firewall-safari.pkg` answered `302` to the signed object on **2026-09-21**, reporting `x-visiq-version: 0.1.11`. The build is **universal (Apple silicon and Intel)**, signed, notarized and stapled. ⚠️ The MDM rollout itself has not been executed end to end: see the warning in the Safari tab.                                                                           |
  | **Firefox**                                    | **No install path.** The extension works in Firefox and is proven there on every release, but nothing signs or distributes it: see the Firefox card below.                                                                                                                                                                                                                                                                                               |

  Because the store route and the self-hosted route carry **different extension ids**, a policy must name a matching id/update-URL pair: see the note under [Fleet deployment](#fleet-deployment-managed-browsers) before you write one.
</Note>

<Note>
  **Where this lives in the product, and how the rollout screen is organised.**

  The card is **Context Firewall (Browser Extension)**, in the **Context Firewall** section of
  the Connectors page, beside the Discovery Sensor.

  Its rollout studio asks **which browser** before it asks which console, because the browser
  decides the mechanism and the console only decides where you paste the result. That ordering
  is what stops the screen offering an Intune-shaped flow for Safari, which has no such flow.
  It also means the work is done **once per browser you run**: there is no cross-browser
  extension-deployment primitive in any MDM, and there is not going to be one.

  * **Chrome & Edge** are a single target. Edge reads the same Chromium policy as Chrome at
    its own path, and every artifact the studio generates carries both, so there is nothing
    separate to deploy for Edge.
  * **Safari** is its own target: a signed package plus a declaration, never
    `ExtensionInstallForcelist`.
  * **Firefox** is shown but **cannot be selected**, and its steps render read-only with no
    copy or download control, because there is no artifact to roll out. See the Firefox card
    above for exactly what is missing.
</Note>

The Context Firewall runs **inside the browser, before a message is sent** to a browser-hosted AI assistant (ChatGPT, Gemini, Copilot, Claude, Perplexity). It masks personal data and blocks secrets in what your staff type, with no message content ever leaving the tab. This page covers getting it onto machines — one at a time, or across a fleet through the same managed-policy channel you already use for other extensions.

## Supported browsers

The extension is a Manifest V3 build, and its interception relies on reaching the page's `MAIN` world. Chrome and Edge run `world: "MAIN"` content scripts natively; Firefox has since 128, so it gets its own Gecko build. Safari does not support that manifest key at all — so the Safari build reaches `MAIN` the classic way instead, by having its isolated content script inject a `<script src>` element pointing at a web-accessible resource. That path works on every engine, and the Safari build asserts it survived conversion on every release.

<CardGroup cols={4}>
  <Card title="Google Chrome" icon="chrome">
    **Supported.** Chrome 128+ on Windows, macOS, Linux and ChromeOS.
  </Card>

  <Card title="Microsoft Edge" icon="edge">
    **Supported.** Edge 128+ (Chromium). Same package as Chrome.
  </Card>

  <Card title="Safari" icon="safari">
    **macOS 13+, on both Apple silicon and Intel**, as a Safari web extension delivered inside a signed, notarized macOS app. Safari drops the `world: "MAIN"` manifest key, so the build uses the injected-`<script>` fallback to reach the page world; `prove/safari-webkit.mjs` proves **WebKit executes it**, and the release asserts the fallback survives conversion. Packaging, signing and notarization are solved end to end. **What has not been confirmed is an intercepted send in a real Safari GUI** (enabling a Safari extension is a manual GUI step no automated proof can perform), so treat Safari interception as engine-proven rather than field-proven, and run [Verify it is actually working](#verify-it-is-actually-working) on the first Mac you deploy to. Fleet rollout is the `.pkg` + managed-extension declaration below, not `ExtensionInstallForcelist`. **Not available on iOS or iPadOS**: Safari there takes extensions only through the App Store. Where the extension is not deployed, the Deployed Sensor's egress proxy still governs the same hosts on the machine: see [Install the Deployed Sensor](/discovery/deployed-sensor#the-context-firewall-on-the-sensor).
  </Card>

  <Card title="Firefox" icon="firefox">
    **Engine-proven, but not distributed: there is no way to install it today.** The Gecko build exists (`extension-firefox/`, carrying the `browser_specific_settings.gecko` id Firefox requires), interception is proven in a **real Firefox** by `prove/firefox-gecko.mjs` on every release, and the package is `web-ext` linted. But nothing signs an `.xpi`, nothing is published to addons.mozilla.org, and VisIQ's distribution endpoint serves no Firefox artifact, so there is no install path we can give you. Firefox 128+ would be the floor when there is one (that is where Gecko gained `world: "MAIN"` content scripts). To govern Firefox users today, use the Deployed Sensor's egress proxy instead: see [Install the Deployed Sensor](/discovery/deployed-sensor#the-context-firewall-on-the-sensor).
  </Card>
</CardGroup>

***

## Manual install (a single machine)

Use this for a proof of concept on one endpoint, to try the firewall before a rollout, or to verify policy behaviour. No MDM is needed.

<Tip>
  **You can evaluate it without an account.** The firewall masks and blocks on its own the moment it is installed — detection runs entirely in the browser, and an unenrolled install makes no network requests at all. Enrolment is only needed if you also want its decisions to appear in your VisIQ dashboard. For a quick look at what it does, install it, skip enrolment, and go straight to [Verify it is actually working](#verify-it-is-actually-working).

  One thing to know if you enrol later: decisions made while unenrolled are **kept on the device, not discarded**, and are sent to your organization when you enrol. Those records never contain the message itself — they carry the rule that fired, the kinds of value detected, counts and timing. If you want an enrolled test to start from a clean slate, remove and reinstall the extension before enrolling.
</Tip>

<Warning>
  **Enrolment requires an organization, not a personal account.** A new VisIQ sign-up starts in your personal account, and generating an enrolment code there is refused with *"Enrol a firewall endpoint from your organization, not your personal account."* Before you enrol, open the account switcher, choose **Create organization** (or switch to your existing one), then generate the code. This prevents a browser from being silently attached to the wrong tenant.
</Warning>

<Tabs>
  <Tab title="Chrome / Edge — from the store">
    The simplest path for a single endpoint: no Developer mode, no file to handle, and it updates itself.

    <Steps>
      <Step title="Open the listing">
        Open the Context Firewall listing link from your VisIQ team and choose **Add to Chrome**. The listing is unlisted, so it is reachable only through that link rather than by searching the store.
      </Step>

      <Step title="(Optional) Enrol the browser to your organization">
        In VisIQ, from your **organization**, open **Context Firewall → Endpoints → Enrol a browser** to generate a single-use code. Then open the extension's options — right-click its toolbar icon and choose **Options**, or go to `chrome://extensions` → **Details** → **Extension options** — and paste the code.

        The message itself never leaves the browser. A decision reports counts, detector kinds and rule ids, and carries masked values (stored encrypted, revealed one at a time) only when your workspace has turned on masked-value retention under **Settings → Context Firewall**.
      </Step>
    </Steps>

    <Note>
      The listing is published and unlisted, serving **0.1.1** as last checked on **2026-09-20**. The self-hosted channel served **0.1.9** on that date, so the store copy lags it: see [Check what each route is serving right now](#check-what-each-route-is-serving-right-now) to compare them yourself at any time.
    </Note>
  </Tab>

  <Tab title="Safari on macOS">
    Safari's extension lives inside a small macOS app, so installing the extension means installing that app. Requires **macOS 13 or later**, on either Apple silicon or Intel.

    <Note>
      **The published installer is universal.** Verified on the 0.1.9 package on **2026-09-20**: the app binary and the Safari extension binary each carry `arm64` and `x86_64`, and the installer declares `hostArchitectures="arm64,x86_64"`. Builds before 0.1.3 were Apple silicon only and refused to install on Intel, so if you scoped an earlier rollout to Apple silicon for that reason, widen it.
    </Note>

    <Steps>
      <Step title="Install the app">
        Download the installer and open it:

        ```bash theme={null}
        curl -fLO https://api.visiqlabs.com/firewall/dist/visiq-context-firewall-safari.pkg
        ```

        It is signed by VisIQ Labs and notarized by Apple, with the ticket stapled, so it installs without an "unidentified developer" warning and without needing a route to Apple's notary service.
      </Step>

      <Step title="Turn the extension on">
        Open **Safari → Settings → Extensions** and tick **VisIQ Context Firewall**. Safari only lets you enable an extension yourself; an app cannot switch its own extension on.

        If it is not listed, quit Safari completely with **⌘Q** and reopen it — Safari only discovers newly installed extensions when it launches, and closing the window is not enough.
      </Step>

      <Step title="Allow it on the assistant sites">
        Turning the extension on is **not** the same as letting it read a site. Open one of the assistants, such as `chatgpt.com`, click the **VisIQ Context Firewall** button in Safari's toolbar, and choose **Always Allow on This Website**. Repeat for each assistant you use.

        Without this the extension is enabled but does nothing on that site. You can review or change these grants later under **Safari → Settings → Extensions → VisIQ Context Firewall → Edit Websites**.
      </Step>

      <Step title="(Optional) Enrol the browser to your organization">
        Generate a code from your **organization** under **Context Firewall → Endpoints → Enrol a browser**, then paste it into the extension's settings, reached from **Safari → Settings → Extensions → VisIQ Context Firewall**.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Chrome / Edge — load unpacked">
    Use this before the store listing is published, or to test a specific build.

    <Steps>
      <Step title="Get the build">
        Obtain the extension `.zip` from your VisIQ team and unzip it to a folder that will not be moved or deleted — Chrome loads it from that location every time it starts.
      </Step>

      <Step title="Load it">
        Open `chrome://extensions` (or `edge://extensions`), turn on **Developer mode**, choose **Load unpacked**, and select the unzipped folder.
      </Step>

      <Step title="(Optional) Enrol the browser to your organization">
        Enrol as in the store tab, with a code generated from your **organization**.
      </Step>
    </Steps>

    <Warning>
      **This often does not work on a company-managed browser.** If **Developer mode** is missing or greyed out, your IT team has disabled it through the `ExtensionDeveloperModeSettings` or `DeveloperToolsAvailability` policy, and **Load unpacked** is blocked with it. That is a policy decision, not a fault: use the store listing instead, or run the evaluation on a Chrome profile or machine your IT team does not manage.

      An unpacked extension also has **no automatic updates**, can be removed by the user, and Chrome may show a warning about developer-mode extensions when it starts. Use it for evaluation only; use the fleet rollout below for anything that needs to stay installed.
    </Warning>
  </Tab>
</Tabs>

***

## Fleet deployment (managed browsers)

Force-installing through your browser-management policy is the way to keep the extension present and non-removable across the estate. It is the same `ExtensionInstallForcelist` mechanism you already use for other managed extensions — a force-installed extension is silently installed, cannot be disabled or removed by the user, and auto-updates. Pick the console that manages your browsers.

<Note>
  **There are two Chrome builds, with two different ids, and a policy must name a matching pair.** An id and an update URL that do not belong together is the one mistake that fails silently: the browser finds nothing to install and the policy sits inert rather than erroring.

  | Route                                      | Extension id                       | Update URL                                            |
  | ------------------------------------------ | ---------------------------------- | ----------------------------------------------------- |
  | **Chrome Web Store** (unlisted listing)    | `ijhpnfmdeeclmfahdmckglbepmiokpfl` | `https://clients2.google.com/service/update2/crx`     |
  | **Self-hosted by VisIQ** (no store review) | `fmjgcgdgkiednbkkjkgcphblopgenjjn` | `https://api.visiqlabs.com/firewall/dist/updates.xml` |

  **For a managed fleet, self-hosted is the recommended route.** It puts you in control of when a fix reaches your estate: VisIQ publishes, your browsers pick it up on their next update check, and no third-party review sits in between. The store route is there for machines you do not manage, and for admins who specifically want Google's review in the loop on every release. Both auto-update; see [Keep it up to date](#keep-it-up-to-date).

  **Deploy exactly one of the two to any given machine.** They are different extension ids, so a browser carrying both runs two independent copies of the firewall and governs every prompt twice. The recommended Intune profile below blocks the other id outright rather than trusting that nobody installs it by hand.

  For Edge sourced from the Edge Add-ons store, use `https://edge.microsoft.com/extensionwebstorebase/v1/crx` and the Edge id, which differs again and is published when that listing goes live. Managed Edge can equally force-install either build above.
</Note>

<Tabs>
  <Tab title="Google Workspace / Chrome Browser Cloud Management">
    In the **Google Admin console** → **Devices → Chrome → Apps & extensions → Users & browsers**, select the org unit, add the extension by id, and set **Installation policy → Force install** (optionally **Force install + pin**). This writes the `ExtensionInstallForcelist` policy for every managed profile in that OU.

    To apply it by Group Policy instead of the console, the equivalent Windows registry / ADMX value is:

    ```
    Software\Policies\Google\Chrome\ExtensionInstallForcelist\1 =
      "ijhpnfmdeeclmfahdmckglbepmiokpfl;https://clients2.google.com/service/update2/crx"
    ```

    On macOS, the same key is delivered as a configuration profile:

    ```xml theme={null}
    <key>ExtensionInstallForcelist</key>
    <array>
      <string>ijhpnfmdeeclmfahdmckglbepmiokpfl;https://clients2.google.com/service/update2/crx</string>
    </array>
    ```
  </Tab>

  <Tab title="Microsoft Intune (Edge or Chrome)">
    Deploy the force-install policy as a **Settings catalog** profile. This is the recommended fleet route, and it uses the self-hosted build.

    <Steps>
      <Step title="Create the profile">
        **Intune → Devices → Configuration → Create → Settings catalog**, for the Windows or macOS platform.
      </Step>

      <Step title="Force-install the self-hosted build">
        For managed Chrome, add **Google Chrome → Extensions → Configure the list of force-installed apps and extensions**. For Edge, add **Microsoft Edge → Extensions → Control which extensions are installed silently**. Edge honours a custom update URL, so the same entry works for both:

        ```
        fmjgcgdgkiednbkkjkgcphblopgenjjn;https://api.visiqlabs.com/firewall/dist/updates.xml
        ```

        The id and the update URL must travel together. An id paired with the wrong URL fails silently: the browser finds nothing to install and the policy sits inert rather than erroring.
      </Step>

      <Step title="Block the other build">
        Add **Extensions → Extension management settings** (`ExtensionSettings`) so a user cannot add the store copy alongside it:

        ```json theme={null}
        {"ijhpnfmdeeclmfahdmckglbepmiokpfl": {"installation_mode": "blocked"}}
        ```

        Without this, a machine can end up running the self-hosted and store builds at once. They are separate extensions with the same code, so every prompt is intercepted twice.
      </Step>

      <Step title="Assign">
        Assign the profile to the device or user groups that should be governed.
      </Step>
    </Steps>

    You can also deliver the same values via an **ADMX-ingested** Administrative Template if your tenant uses the Edge/Chrome ADMX templates. On Windows the equivalent registry value is `Software\Policies\Google\Chrome\ExtensionInstallForcelist\1`.

    <Note>
      **Chrome force-installs an off-store extension only on a managed browser.** Intune-delivered policy is exactly that, so this route works on your managed estate and will not work on an unmanaged personal machine. Use the store route there.
    </Note>

    <Note>
      **This route is proven on a real Intune-managed device.** Verified 2026-09-17 on an Entra-joined, Intune-enrolled Windows 11 client: the profile above put the forcelist entry into `HKLM\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallForcelist`, and Edge installed **VisIQ Context Firewall 0.1.3** from the VisIQ update manifest into the signed-in user's profile, with `host_permissions` limited to `https://api.visiqlabs.com/*`, no broad optional host, and **no copy of the store build** (the blocklist step above did its job). Read from the device itself, not from Intune reporting the assignment as successful.

      **The two browsers do not have the same evidence behind them, so here is exactly what each has.**

      | browser    | on a real Intune-managed device                                                                                                                                     | in a container under a real managed policy |
      | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
      | **Edge**   | **force-install observed directly**: the extension directory count went from 0 to 1 once Edge started, at 0.1.3, with the store copy absent                         | proven, Edge 153 at its own policy path    |
      | **Chrome** | the extension **ran and enrolled itself** from that machine, reporting its version and a Chrome user agent to the control plane; the directory count was never read | proven, Chrome 153                         |

      Both are real evidence that the extension reached the browser. Only Edge's was read from the filesystem before and after, which is the stronger form, and saying so is cheaper than discovering the difference during a rollout.
    </Note>

    <Warning>
      **A delivered policy is not an installed extension.** Chromium force-installs at **browser startup**. On a machine whose browser is closed when the policy lands, the registry entry is present and the extension is absent, and that is the normal intermediate state rather than a failure. Measured on the device above: the extension count was `0` with the policy already in place, and became `1` the moment Edge ran. If you are checking coverage during a rollout, count browsers that have been **started** since the assignment, not devices that have received it.
    </Warning>
  </Tab>

  <Tab title="Safari on macOS">
    **Safari does not use `ExtensionInstallForcelist`** — that key is a Chromium policy and Safari ignores it entirely. A Safari extension lives inside a macOS app, so a fleet rollout is two steps: install the app, then force-enable the extension it contains.

    <Warning>
      **This Safari rollout has NOT been executed end to end.** Every value below is taken from the shipped 0.1.9 package itself rather than written from memory (the bundle ids, the team id, the minimum OS, the signature and notarization state are all read out of the installer), and the Chromium routes on this page are proven by execution. The Safari path is not: no Mac has been available to push the package through an MDM, install it, and confirm Safari enables the extension. Treat it as carefully prepared and unverified, and run [Verify it is actually working](#verify-it-is-actually-working) on the first Mac you deploy to rather than assuming the fleet is covered.
    </Warning>

    <Steps>
      <Step title="Deploy the signed package">
        Push **`VisIQ-Context-Firewall-<version>.pkg`** as a custom / in-house macOS app from your MDM (Jamf: a package in a Policy or a Blueprint; Intune: **Apps → macOS app (PKG)**; Kandji: **Custom Apps**; Mosyle: **Custom Package**). It installs `VisIQ Context Firewall.app` to `/Applications`.

        The package is signed with our Developer ID Installer certificate and notarized by Apple, so Gatekeeper accepts it with no user prompt and no "unidentified developer" warning. The notarization ticket is **stapled**, so it installs on a Mac with no route to Apple's notary service.

        Intune's **macOS app (PKG)** form asks for detection and targeting values. Taking them from the 0.1.9 package itself:

        | Field                                     | Value                                                                     |
        | ----------------------------------------- | ------------------------------------------------------------------------- |
        | App bundle ID                             | `com.visiqlabs.context-firewall`                                          |
        | App version                               | `0.1.9` (the `CFBundleShortVersionString`, and `CFBundleVersion` matches) |
        | Minimum operating system                  | macOS 13.0                                                                |
        | Package identifier (product)              | `com.visiqlabs.context-firewall.pkg`                                      |
        | Component identifier (inside the package) | `com.visiqlabs.context-firewall`                                          |
        | Signing team                              | `57998XYMAD` (VisIQ Labs, LLC)                                            |

        Get the bundle id wrong and Intune never detects the install, so it re-pushes the package on every evaluation cycle.

        <Note>
          **Two identifiers, one letter apart, and they are not interchangeable.** The installer carries both, which is why this table lists both rather than picking one:

          * the **product** identifier `com.visiqlabs.context-firewall.pkg` is what `productbuild --identifier` stamps, and it appears as `<product id="...">` in the package's Distribution file
          * the **component** identifier `com.visiqlabs.context-firewall` is what `PackageInfo` inside the package carries, and it is also the app's bundle id

          Read out of the published 0.1.9 package on 2026-09-20. An earlier version of this table showed only the component identifier, after it was read from `PackageInfo` and the `.pkg` suffix was mistaken for a filename that had leaked into an identifier field. It had not: both strings are real, and they name different things.
        </Note>

        **Scope the deployment to macOS 13.0 or later. Both Apple silicon and Intel are supported.** The published 0.1.9 installer is universal: the app binary and the Safari extension binary each carry `arm64` and `x86_64`, and the package declares `hostArchitectures="arm64,x86_64"`. Builds before 0.1.3 were Apple silicon only and refused to install on Intel, so if you narrowed an existing smart group for that reason, widen it.

        Installing the app does not switch the extension on by itself. Without the next step, each user would still have to enable it manually — which is how extension rollouts stall at partial coverage.
      </Step>

      <Step title="Force-enable the extension by declaration">
        Apple manages Safari extensions through a **declarative device management** configuration, `com.apple.configuration.safari.extensions.settings`. Deliver it as:

        * **Jamf Pro 11.15+** — a **Blueprint** containing the Safari Extension Settings declaration
        * **Microsoft Intune** — **Devices → Configuration → Settings catalog → Declarative Device Management → Safari Extension Settings**
        * **Kandji / Mosyle / other** — the vendor's Safari extension management profile, or a custom declaration if the console accepts raw DDM

        The extension is identified by the composed key `Identifier (TeamIdentifier)`. For the Context Firewall that is exactly:

        ```
        com.visiqlabs.context-firewall.Extension (57998XYMAD)
        ```

        Settings to apply:

        | Key               | Value                     | Why                                                                                        |
        | ----------------- | ------------------------- | ------------------------------------------------------------------------------------------ |
        | `State`           | `AlwaysOn`                | Present and **not user-disableable**. `Allowed` would let someone switch the firewall off. |
        | `PrivateBrowsing` | `AlwaysOn`                | A private window is exactly where someone pastes what they would rather not have logged.   |
        | `AllowedDomains`  | the assistant hosts below | Grants access without the per-site "Always Allow" prompt each user would otherwise see.    |

        If your console takes a raw declaration, this is the whole payload:

        ```json theme={null}
        {
          "Type": "com.apple.configuration.safari.extensions.settings",
          "Identifier": "com.visiqlabs.context-firewall.managed-extension",
          "Payload": {
            "ManagedExtensions": {
              "com.visiqlabs.context-firewall.Extension (57998XYMAD)": {
                "State": "AlwaysOn",
                "PrivateBrowsing": "AlwaysOn",
                "AllowedDomains": [
                  "*claude.ai",
                  "*chatgpt.com",
                  "*chat.openai.com",
                  "*gemini.google.com",
                  "*copilot.microsoft.com",
                  "*copilot.cloud.microsoft",
                  "*m365.cloud.microsoft",
                  "*perplexity.ai",
                  "*aistudio.google.com",
                  "*console.anthropic.com",
                  "*platform.openai.com"
                ]
              }
            }
          }
        }
        ```

        <Warning>
          **Note the wildcard form: `*chatgpt.com`, not `*.chatgpt.com`.** Apple's format has no dot after the asterisk. `*chatgpt.com` matches the domain and its subdomains but not `mychatgpt.com`. The dotted spelling is the one most people write from habit, and it matches **nothing** — the profile applies cleanly and the extension is silently denied every host.
        </Warning>
      </Step>
    </Steps>

    #### Keeping the Safari fleet current

    **Safari does not update this extension on its own.** Chrome polls for new versions itself; nothing on a Mac checks for updates to an app installed this way. So a Safari fleet stays current only if your MDM re-fetches the installer and reinstalls when the version changes.

    VisIQ serves the current installer at a URL that **never changes between releases**:

    ```
    https://api.visiqlabs.com/firewall/dist/visiq-context-firewall-safari.pkg
    ```

    <Warning>
      **This URL always serves the CURRENT release, never a pinned one.** That is what makes the Installomator label below work, and it also means you cannot use it to hold a fleet on an older version. Check what it is offering before downloading, using the `x-visiq-version` header below, and verify the download against the `x-visiq-sha256` header if your tooling supports it.
    </Warning>

    The current version is returned as a header on that URL, so you can check it without downloading anything:

    ```bash theme={null}
    curl -sI https://api.visiqlabs.com/firewall/dist/visiq-context-firewall-safari.pkg | grep -i x-visiq-version
    ```

    The most common way to automate this is **[Installomator](https://github.com/Installomator/Installomator)**, the open-source installer script that runs under Jamf, Kandji, Mosyle and Intune alike. Add this label and schedule it the way you schedule your other Installomator items:

    ```bash theme={null}
    visiqcontextfirewall)
        name="VisIQ Context Firewall"
        type="pkg"
        downloadURL="https://api.visiqlabs.com/firewall/dist/visiq-context-firewall-safari.pkg"
        appNewVersion=$(curl -sI "$downloadURL" | awk -F': ' 'tolower($1)=="x-visiq-version"{print $2}' | tr -d '\r')
        expectedTeamID="57998XYMAD"
        ;;
    ```

    `expectedTeamID` makes Installomator refuse any installer not signed by VisIQ Labs, so a tampered or mis-served download is rejected rather than installed. It compares `appNewVersion` against the installed app's version and only reinstalls when they differ.

    <Note>
      **Kandji Auto Apps and Jamf App Installers will not pick this up.** Both are curated catalogs of vendors the MDM itself maintains, not trackers of an arbitrary URL. Use Installomator, or your MDM's scripted-policy equivalent, instead.
    </Note>

    <Warning>
      **The force-enable step needs macOS 15 (Sequoia) or later, on a supervised Mac** — in practice, one enrolled through Automated Device Enrollment. Apple introduced Safari extension management in macOS 15 and there is no earlier equivalent.

      On **macOS 13 or 14**, or on a Mac enrolled manually rather than through ADE, the `.pkg` still installs and the extension still works — but each user must enable it themselves once, in **Safari → Settings → Extensions**. Plan for that prompt rather than being surprised by it, and check your macOS version spread before promising full coverage.
    </Warning>
  </Tab>

  <Tab title="Self-hosted (no store review)">
    Force-install the build VisIQ serves directly. Same `ExtensionInstallForcelist` mechanism and the same console screens as the store route — only the id and the update URL change, and they must change **together**.

    ```
    fmjgcgdgkiednbkkjkgcphblopgenjjn;https://api.visiqlabs.com/firewall/dist/updates.xml
    ```

    In the **Google Admin console**, adding an extension "by id" assumes the store, so use **Add from a custom URL** (Devices → Chrome → Apps & extensions → Users & browsers → **+** → *Add Chrome app or extension by ID* → set **From a custom URL**) and supply the update URL above, then set **Installation policy → Force install**.

    On Windows by Group Policy, and on macOS by configuration profile, it is the same key you would use for the store, carrying the pair above:

    ```
    Software\Policies\Google\Chrome\ExtensionInstallForcelist\1 =
      "fmjgcgdgkiednbkkjkgcphblopgenjjn;https://api.visiqlabs.com/firewall/dist/updates.xml"
    ```

    <Warning>
      **Chrome only force-installs an off-store extension on a managed browser.** The policy must arrive through real device or browser management (cloud-managed Chrome, Group Policy, or an MDM configuration profile). On an unmanaged machine Chrome ignores an off-store forcelist entry — that restriction is Chrome's, and it is why the store route exists alongside this one.
    </Warning>

    The extension is signed with VisIQ's own key; the id above is derived from that key, so an update that was not signed by us cannot take its place.
  </Tab>

  <Tab title="Other MDM (Jamf, generic)">
    Any MDM that can push a managed-browser configuration profile can force-install the extension: deliver the `ExtensionInstallForcelist` (Chrome) or the Edge `ExtensionInstallForcelist` key with the single entry `ijhpnfmdeeclmfahdmckglbepmiokpfl;<update-url>`. On Jamf, deploy it as a **Custom Settings** payload for `com.google.Chrome` or `com.microsoft.Edge`.
  </Tab>
</Tabs>

### Pre-enrol the whole fleet

A force-installed extension is present on every managed browser, but each still needs to be **enrolled** to your tenant to report. Rather than enrolling each by hand, push the enrolment values alongside the force-install policy using the browser's **managed configuration**, so a machine is governed the moment the extension lands. Generate the fleet token in **Context Firewall → Endpoints**.

<Note>
  **Shipped in 0.1.9, published 2026-09-20.** Verified by downloading what the channel serves and unpacking it: the reporting driver in the published `.crx` carries the managed-configuration read and the enrolment call. Between 2026-09-17 and that release the channel served 0.1.3, which did **not**, so a policy written during that window silently did nothing. If you built one then and saw no endpoints appear, it should begin working once each browser restarts and picks the new build up.

  **Requires 0.1.9 or later.** Check what your machines are actually running with [Verify it is actually working](#verify-it-is-actually-working) before concluding a policy is at fault.
</Note>

The extension reads exactly three keys:

| Key                | What it is                                                                                   |
| ------------------ | -------------------------------------------------------------------------------------------- |
| `controlPlane`     | Your VisIQ API base, e.g. `https://api.visiqlabs.com`. No trailing slash.                    |
| `fleetEnrollToken` | The multi-use fleet token you generated. Copy it when it is shown; it is displayed once.     |
| `label`            | The name every endpoint enrolled by this policy reports under, e.g. `Managed Chrome - EMEA`. |

<Warning>
  **The id in this policy must be the id of the route you force-installed from.** Managed configuration is delivered *to a specific extension id*, so a policy written against the store id while the browser installed the self-hosted build (or the reverse) is delivered to an extension that is not there. Nothing errors: the extension installs, finds no configuration, and stays unenrolled, which is indistinguishable from a rollout still in progress.
</Warning>

**Chrome and Edge**: the values live under `3rdparty.extensions.<extension id>`. For the self-hosted route documented above:

```json theme={null}
{
  "3rdparty": {
    "extensions": {
      "fmjgcgdgkiednbkkjkgcphblopgenjjn": {
        "controlPlane": "https://api.visiqlabs.com",
        "fleetEnrollToken": "<FLEET_TOKEN>",
        "label": "Managed Chrome - EMEA"
      }
    }
  }
}
```

Deliver it as **Google Admin → the extension → Policy for extensions**, as the `3rdparty` key of an OS-level policy (macOS configuration profile for `com.google.Chrome`, or `com.microsoft.Edge` for Edge), or on Linux in `/etc/opt/chrome/policies/managed/`. On Windows by Group Policy the same values are registry entries under:

```
HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\fmjgcgdgkiednbkkjkgcphblopgenjjn\policy
```

If you installed from the Chrome Web Store listing instead, substitute `ijhpnfmdeeclmfahdmckglbepmiokpfl` in both places.

<Warning>
  **Microsoft Intune cannot deliver this through the Settings catalog, and that is the one place you would look for it.** The force-install entry above IS a Settings catalog setting, so it is reasonable to expect its companion to be one too. It is not: there is no Chromium managed-configuration (`3rdparty`) setting in the Edge or Chrome settings catalog at all, verified against the live setting definitions on 2026-09-19. Searching for it returns policies whose names merely contain *managed* or *third party*, which is worse than an empty result because one of them looks close enough to try.

  Deliver the three values to Windows machines as **registry writes** instead, through whichever mechanism your estate already uses for registry state: Group Policy preferences, an Intune **remediation** (Devices → Scripts and remediations), or a platform script. Write them under the browser you force-installed into:

  ```
  HKLM\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\fmjgcgdgkiednbkkjkgcphblopgenjjn\policy
  HKLM\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\fmjgcgdgkiednbkkjkgcphblopgenjjn\policy
  ```

  with `controlPlane`, `fleetEnrollToken` and `label` as `REG_SZ` values. The extension reads them through `chrome.storage.managed` exactly as it reads a policy delivered any other way.

  ⚠️ **Verify enrolment from the product, not from Intune.** A green assignment means the policy was delivered, never that a browser enrolled: the browser must also start and read it. The honest check is that an endpoint appears under **Context Firewall → Endpoints**, or that `GET /api/firewall/endpoints` returns one.
</Warning>

**Firefox**: the same three keys, but Firefox spells the tree `3rdparty.Extensions` (capital *E*) and keys it by the add-on's gecko id, in `policies.json`:

```json theme={null}
{
  "policies": {
    "3rdparty": {
      "Extensions": {
        "context-firewall@visiqlabs.com": {
          "controlPlane": "https://api.visiqlabs.com",
          "fleetEnrollToken": "<FLEET_TOKEN>",
          "label": "Managed Firefox - EMEA"
        }
      }
    }
  }
}
```

<Note>
  The Firefox form is documented here because the keys are the same and the policy is worth preparing, but as the [Firefox card](#supported-browsers) above says, there is **no signed Firefox build to install today**, so this policy has nothing to configure yet.
</Note>

You can generate all of these prefilled with your own tenant's values (including the Windows `.reg` and the macOS profile) from **Context Firewall → Deploy** in the dashboard, rather than editing the examples above by hand.

***

## Keep it up to date

Each supply route updates differently, and one of them is not automatic. Know which you are on before you assume a fix has reached the fleet.

| Route                         | How a new version arrives                                                                                                                        | Typical delay                                                                                                                    |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| **Chrome Web Store**          | Chrome checks the store on its own and installs the new version. Nothing to do.                                                                  | Hours, plus Google's review of each release before that clock even starts                                                        |
| **Self-hosted (Chrome/Edge)** | Chrome and Edge poll `updates.xml` on their normal update cycle and install the newer version it advertises. Nothing to do.                      | Hours. See the note below before assuming a restart shortens it, and read the honesty note on what we have and have not observed |
| **Safari**                    | **Not automatic.** A macOS app does not check for its own updates, so your MDM must re-fetch the installer and reinstall when the version moves. | Whatever your MDM's schedule is                                                                                                  |

<Note>
  **Forcing a Chrome update sooner.** On a managed browser, visiting `chrome://extensions`, enabling **Developer mode** and choosing **Update** requests a check against whichever update URL the policy names. Useful for confirming a rollout rather than waiting out the cycle.

  ⚠️ **That button is throttled, and the throttle is silent.** Measured 2026-09-17: driving the same call the button makes, seconds after an install, reported success while making **zero** requests to the update server. Nothing tells you it was suppressed. If you press Update and the version does not move, wait about ten minutes and press it again before concluding the rollout is broken, and confirm against your server's own logs rather than the button's response.

  ⚠️ **Restarting the browser is NOT a reliable way to pull an update forward.** Measured 2026-09-17 against a real managed policy: Chrome queries the update manifest at startup only when the forced extension is **missing**. Once it is installed and the policy is satisfied, a restart produced no update query at all, and the only remaining trigger was the periodic cycle. If you need a specific version on a machine now, use the **Update** button above rather than telling people to reboot.
</Note>

<Note>
  **What we have proven about updating, and what we have not.** The install half is proven on a real Intune-managed device (see Fleet deployment). The update half is not, and we would rather say so than let the table above read as field-tested:

  * **Proven.** The update manifest and the signed package behind it are correct and live: a channel advertising a newer version serves a real, well-formed CRX whose own manifest carries that version, verified by unpacking what the server returns rather than by trusting the XML.
  * **Proven.** A restart does not pull an update forward, and the Update button is silently throttled. Both were measured, and both are written up above because they are the two assumptions most likely to make you misread a rollout.
  * **NOT observed end to end.** We have not yet watched an already-installed extension move to a newer version on its own on a managed device. The periodic cycle is Chromium behaviour we rely on rather than something we have timed in the field: the test machine available to us powers down when idle, and the browser's update timer is held in memory and restarts with the browser, so the machine has never stayed up long enough for one cycle to elapse.

  Treat automatic update as the documented behaviour of the browser rather than a VisIQ measurement. If a fix matters on a specific machine today, verify the version on that machine using [Verify it is actually working](#verify-it-is-actually-working) instead of assuming the cycle has run.
</Note>

**For Safari, point your MDM at the stable URL rather than a versioned file**, so a release does not need a change on your side. The URL, the version header it returns and a ready-made Installomator label are all under *Keeping the Safari fleet current* in the Safari tab of [Fleet deployment](#fleet-deployment-managed-browsers).

<Warning>
  **A pinned version is a fleet that never gets a fix.** If your MDM caches the installer or you host a copy internally, put a recurring check on that stable URL — otherwise a detection fix we publish will never reach the machines that most need it.
</Warning>

### Check what each route is serving right now

You do not have to take our word for which version your fleet is converging on. Each route answers for itself:

```bash theme={null}
# Self-hosted (Chrome and Edge): the version attribute is what a managed browser converges on
curl -s https://api.visiqlabs.com/firewall/dist/updates.xml

# Chrome Web Store: the redirect filename ends _<major>_<minor>_<patch>_0.crx
curl -s -o /dev/null -w '%{redirect_url}\n' \
  "https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&prodversion=153.0.0.0&x=id%3Dijhpnfmdeeclmfahdmckglbepmiokpfl%26uc"

# Safari: the installer reports its version in a header, so you can check without downloading
curl -sI https://api.visiqlabs.com/firewall/dist/visiq-context-firewall-safari.pkg | grep -i x-visiq-version
```

<Note>
  **The two Chromium routes can legitimately disagree, and usually will.** A store release waits on Google's review before it can even begin rolling out; the self-hosted channel does not. Observed on 2026-09-20: self-hosted served **0.1.9** while the store served **0.1.1**. That gap is the reason self-hosted is the recommended route for a managed fleet, and it is also why the two ids must never be mixed on one machine.
</Note>

***

## Verify it is actually working

Do this once per browser type after the first rollout, before you tell anyone they are covered. It takes a minute and it is the difference between "the extension is installed" and "the firewall is running".

<Steps>
  <Step title="Open a governed assistant">
    Go to any of the covered sites — `chatgpt.com`, `claude.ai`, `gemini.google.com`, `copilot.microsoft.com` or `perplexity.ai`.
  </Step>

  <Step title="Check that masking fires">
    Type this into the message box and send it:

    ```
    Please charge the card 4111 1111 1111 1111 for the renewal.
    ```

    The message should send with the card number replaced, and a notice should appear saying what was masked. `4111 1111 1111 1111` is the standard Visa **test** number, not a real card — the detector validates a Luhn checksum, so an ordinary 16-digit order number is not mistaken for one.
  </Step>

  <Step title="Check that blocking fires">
    Then send:

    ```
    Use AKIAIOSFODNN7EXAMPLE with secret wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    ```

    This should be **blocked outright** rather than masked. Credentials are stopped, because a partially masked key is still a leaked key if its shape gives it away. (This is Amazon's own published example key pair, not a live credential.)
  </Step>

  <Step title="Check that it stays quiet otherwise">
    Send an ordinary question such as `What is the capital of France?`. Nothing should happen — no notice, no delay. The firewall acts only on sensitive content, and a rollout where users see notices on ordinary messages will not survive contact with the estate.
  </Step>
</Steps>

<Warning>
  **If nothing happens on the first two steps, the extension is installed but not intercepting.** Do not assume coverage. On Safari, the usual cause is that the extension is enabled but has not been granted access to the site — open **Safari → Settings → Extensions → VisIQ Context Firewall** and confirm the assistant hosts are allowed, or push the `AllowedDomains` declaration above so the grant is not left to each user. On Chrome or Edge, confirm the browser is version 128 or later; older versions cannot run the interception and the extension will sit inert.
</Warning>

***

## After install

Open **Context Firewall → Endpoints** to see which browsers have enrolled and what state each is in. A browser that force-installed but has not yet enrolled shows as **Not reported recently** until its first heartbeat arrives. Discovered-but-unenrolled browsers are listed separately — the two populations are deliberately not joined, because they are measured differently.
