A Manifest V3 extension for Brave and Chromium. It opens .mht, .mhtml, and extensionless MHTML archives in a local, sandboxed viewer. No build step or server is needed.
- Open
brave://extensions. - Turn on Developer mode at the upper right.
- Click Load unpacked and select the downloaded or cloned repository folder, the one containing
manifest.json. - Open the extension's Details page and turn on Allow access to file URLs. This is needed for automatic detection and for the viewer to read a
file://archive. The manual picker can read a file you select even if file URL access is off. - Pin Local MHTML Viewer from Brave's Extensions menu if you want its button visible.
Open an archive using Ctrl+O, drag and drop into a tab, or a file:// URL. When Brave reports a .mht or .mhtml navigation, the extension changes that tab to its viewer. Extensionless archives are recognized from their MIME structure if Brave displays them as raw text; the From header can name any producer or be absent. If it does not open automatically, click the extension button, click Open MHTML, and choose the archive in the viewer. The button stays available in the viewer for another file. After updating these unpacked source files, click Reload on the extension's card at brave://extensions before trying again.
webNavigation.onBeforeNavigate reports a top-frame file:// navigation. The service worker checks the .mht or .mhtml suffix and calls tabs.update to open viewer.html?source=… in the same tab. onCommitted is a second chance when the original file commits first. For extensionless files, a local-file content script examines up to the first 8192 characters after Brave creates a raw-text page. It checks for a multipart/related MIME header with a boundary and an HTML root (type=text/html or a first HTML part). It does not rely on From: <Saved by Blink> or a filename suffix, and sends only a fixed detection marker to the worker. The viewer fetches the local file and parses it in memory. Only webNavigation and file:///* host access are requested. No tabs, webRequest, declarativeNetRequest, or remote host permission is needed.
Browser limit: webNavigation is a notification, not a cancelable request hook. MV3's blocking webRequest is unavailable to ordinary unpacked extensions, and the extension cannot replace the browser's file document before rendering as a guaranteed atomic operation. Extensionless detection necessarily happens after raw text appears. A brief raw-source view or a missed event is possible. Browser file-access settings can also prevent automatic detection or reading. The manual picker is the dependable path because it receives the selected file directly. Ctrl+O and desktop drag/drop use browser navigation, but their native UI gestures are not directly exercised by the automated test.
The parser supports multipart MIME, nested multipart containers, base64, quoted-printable, folded MIME headers, Content-Location, Content-ID (cid:), relative URLs, MIME and common HTML charset declarations, embedded CSS, CSS @import, CSS url(), images, fonts, audio, and video. Bundled resources become data: URLs; missing or external resources are removed. Inline styles and document layout are retained where practical. Scripts, forms, frames, objects, external links, and network dependent features do not run. Unusual MIME variants, malformed archives, and dynamic pages may differ from the original browser capture.
The synthetic fixture is fixture/sample.mhtml. It includes quoted-printable UTF-8 HTML, an embedded stylesheet with a relative background image, a base64 CID image, an archived script, and an external image URL.
With Python Playwright installed and Brave at /usr/bin/brave, run python3 tests/browser_test.py from this folder. It launches Brave headlessly with a fresh temporary profile and the unpacked extension, checks manual file input and the toolbar button, then navigates to .mhtml, .mht, and extensionless copies of the fixture using file:// URLs and checks automatic redirection and rendering. It also verifies detection with a non-Blink From header and that an ordinary text file stays untouched. It checks that the archived script did not run, the iframe has an opaque origin, and the external image caused no request. On this machine, Brave 153.1.95.104 passed those checks. The test does not automate Brave's native Ctrl+O dialog, desktop drag and drop, or the user's existing Brave profile and file-access toggle.
See PRIVACY.md. Archive bytes are held only in the viewer tab's memory. The archived HTML is put in an iframe with an empty sandbox attribute, giving it a unique opaque origin without script, form, top-navigation, or download privileges. A second CSP inside the frame denies network fetches, scripts, objects, and forms. Resource URLs are resolved only against MIME parts and converted to data: URLs. The archived document cannot call extension APIs or read arbitrary local files. The viewer's own extension code is packaged locally and its CSP allows only packaged scripts.