个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证A private, ad-free Mac browser on WebKit: blocks ads, trackers, pop-ups and fingerprinting before they load.
翻译准备中
正在加载完整 README。
查看完整 README ↗A macOS browser built for security first, blocking second, and speed close behind. Native Swift/SwiftUI on WebKit, with a filter compiler, fingerprinting defence and security policy layer that were each verified against the real engine rather than written from documentation.
Engine: WebKit, not Chromium. A Chromium fork needs ~70–100 GB of build space (this machine had 96 GB free) and hours per build; embedding Chromium via CEF means shipping and patching a ~250 MB engine yourself. WebKit is patched by Apple with the OS, compiles content-blocking rules to native bytecode, and exposes engine-level security switches (no-JIT, Lockdown) that no extension can reach. The trade-off: some sites sniff for Chrome, and there is no browser-extension support.
Numbers below come from the test suite, not estimates. Network-dependent tests run with
TEST_RUNNER_IW_NETWORK_TESTS=1 (see Testing).
RealWorldBlockingTests)The same page loads 20 real tracker endpoints and 7 legitimate resources, once with Icy Weasel's lists (A) and once with none (B). B proves each endpoint is reachable, so a failure under A is attributable to blocking.
| Result | |
|---|---|
| Reachable trackers blocked | 19 / 19 (Google Analytics & Tag Manager, DoubleClick, AdSense, Meta Pixel, Hotjar, Criteo, Taboola, Outbrain, comScore, Pinterest, TikTok, LinkedIn, Clarity, Amazon Ads, Mixpanel, HubSpot, Bing UET, Quantcast) |
| Legitimate resources broken | 0 / 7 (jQuery, lodash, React, htmx, Wikimedia, GitHub, MDN) |
| Requests completed | 27 → 7 |
RealWorldBlockingTests, 6 alternating runs per arm, medians)| Page | Requests | Third-party hosts | DOMContentLoaded | load event |
|---|---|---|---|---|
| speedtest.net | 95 → 25 | 52 → 6 | 608 → 616 ms | 616 → 629 ms |
| cnn.com | 87 → 77 | 15 → 11 | 612 → 466 ms | 1098 → 1092 ms |
| theguardian.com | 86 → 76 | 4 → 3 | 255 → 369 ms | 414 → 561 ms |
Blocking removes most requests and third-party connections on ad-heavy sites. Its effect on load time is mixed, and reported as measured: faster on CNN, neutral on Speedtest, and about 0.1 s slower on The Guardian — a lean page with little to block, where WebKit's per-request rule matching is the dominant cost. See Speed.
WebKit's ignore-previous-rules cancels only rules above it in the same list, and
lists cannot see each other. The compiler turns that into structure:
$generichide
exceptions → site-specific hiding → $elemhide exceptions → $document exceptions.
Network lists: blocks → request exceptions → $document exceptions. A
$generichide exception therefore can never unblock a network request — the bug
that briefly let @@||bing.com^$generichide unblock bat.bing.com everywhere.resource-type: document, not .* + if-domain. A .* trigger makes a rule a
candidate on every resource load; 12k of them cost The Guardian ~120 ms of parsing.[ compiles), and a merged rule with one such
selector silently hides nothing.||parent^
rule is removed; this cannot change any outcome.RealFilterListTests, CompilerTests, WebKitIntegrationTests)| Result | |
|---|---|
| Source lines (7 default lists) | 264,852 |
| Rules active | 230,291 in 3 compiled lists (2 network, 1 cosmetic) |
| Rules WebKit rejected | 0 |
| Unsupported syntax, skipped and counted | 5,879 (mostly 2,786 uBO +js() scriptlets) |
| Duplicates removed across lists | 22,662 |
Redundant rules pruned (covered by a ‖host^ rule) | 7,831 |
| First compile (per list or app update) | ~19 s, in the background, with a bundled seed list active meanwhile |
| Relaunch with compiled lists cached | ~5 ms |
| Compiled lists on disk | ~115 MB (in the app's container) |
FingerprintDefenseTests.testInsertionAndInterpreterCost)| Workload | Stock WebKit | Icy Weasel default | |
|---|---|---|---|
| 10k-cell table build + layout | 39 ms | 48 ms | 1.23× |
| Fingerprinting shim alone on that render | 39 ms | 42 ms | 1.08× |
Cosmetic filter stylesheet, style + layout (StyleCostExperiment) | 7 ms | 8 ms | ~1 ms |
| 30M integer operations (pure compute) | 7 ms | 647 ms | ~90× |
Same, switched per navigation as tabs do (ProductionPathTests) | 7 ms | 487 ms | 70× |
That last row is the one real speed cost, and it is deliberate — see JavaScript engine.
securityRestrictionMode = .maximizeCompatibility,
macOS 26.4+). JIT compilers turn attacker-supplied script into machine code at run
time and are the most exploited part of any browser. Also widens memory tagging (MTE)
on supporting hardware. Per-site override; Lockdown mode available..errorOnFailure policy, plus a make-https
rule for subresources and HSTS-preload upgrades. Failures are classified against
error codes observed on real hosts (HTTPSPolicyProbe): a site without HTTPS gets an
interstitial with a per-site escape hatch; an offline network or unknown host is
never blamed on HTTPS. There is deliberately no bypass for invalid certificates.Sec-GPC: 1, macOS 27).fbclid, gclid,
utm_*, mc_eid, 90+ others; host-scoped ones like YouTube's si only on their
host). Idempotent, so the cancel-and-reload cannot loop.Scope was set by measuring stock WebKit first (FingerprintSurfaceProbe). WebKit
already caps hardwareConcurrency at 8, returns the spec-standard plugin list, masks
the GPU as "Apple GPU" and hides local IPs behind mDNS — patching those would make
Icy Weasel users more identifiable. What it leaves exposed is readback, so the shim
covers exactly that:
getImageData, toDataURL, toBlob, WebGL readPixels) and audio
(getChannelData, copyFromChannel, AnalyserNode) readback noise.srcdoc/blob: frames, window[i] access, innerHTML, replaceChild, nested and
parser-inserted frames.toString/name/length/descriptors and
error messages preserved; null-prototype Proxy handlers (inherited traps would leak
the native function); builtins captured before page scripts run, so a page replacing
WeakMap.prototype.get receives nothing.Saved to ~/Downloads with the quarantine attribute (Gatekeeper checks before first
open), server-supplied names reduced to a last path component (no ../ escapes),
never overwrite, never auto-open.
The sparkle button at the top right opens a panel that answers questions about the
current page. Models come from two places (Sources/Assistant/AIProviders.swift):
start_llama_server.sh runs), MLX
(8081, Bonsai's start_mlx_server.sh), LM Studio (1234), Jan (1337), GPT4All (4891)
and 8000, plus any loopback address added in Settings > AI. Whatever answers is listed
with its models, so new tools and models appear without an app update. An installed
but stopped Ollama gets a Start button.Page content is handed to the model as untrusted data, and the model has no tools, so a
hostile page can at worst make an answer wrong (PracticalBrowserTests plants an
injection in the page and checks the model still answers from the facts).
AIProviderTests cover discovery, streaming, consent and key handling; its
Bonsai-style end-to-end test runs against scripts/fake-ai-server.py, because the app
sandbox can't host a server itself.
The speaker in the toolbar (⌥⌘R) reads the selected text, or else the page's article:
<article>/<main> when the page marks one, otherwise the block holding the most
paragraph text, skipping navigation, asides, hidden elements and code
(Sources/ReadAloud/PageReader.swift). The text is read in the isolated world and
spoken on the Mac; nothing is sent anywhere.
Pages are read in their own language, recognised from the text. The voice is Phonon, Gradium's open on-device model, on the Neural Engine through Core ML; Settings > General chooses the model, a voice per language and the speed; choosing a voice or model plays a sample in it:
scripts/export-phonon-multilingual.sh
and checked against a pinned SHA-256 before it is unpacked. Its licence and a note of the
changes (converted to Core ML, catalog voices added) travel inside it. Not published
yet: until its checksum is pinned in PhononModelDownload, Settings doesn't offer it.A language the chosen model doesn't speak is read by Apple's voice for that language.
Audio streams in 80 ms chunks, so reading starts at once, and generation runs at most 20 s
ahead of playback. Phonon's core is arm64 only and its models need macOS 15, so on Intel
Macs and macOS 14 the same button reads with Apple's voices. ReadAloudTests check the
extraction on real pages, language recognition and routing, that Phonon produces speech,
plays to the end and stops cleanly, and that a download is unpacked and checked.
Sign-ins are offered for saving after a real submit (Resources/JS/password-manager.js,
isolated world; synthetic events from the page are ignored) and stored as internet
passwords in the login keychain (Sources/Passwords). They fill only when the user
clicks the key in the address bar, only into the frame that showed the form, and only
for the exact HTTPS host they were saved for. Showing or copying one in Settings asks
for the Mac's password or Touch ID. Private windows never save. Google and Safari
exports (CSV) can be imported; Google's own sync is only available inside Chrome.
WebKit refuses window.open without a user gesture; Icy Weasel also blocks the
pop-under pattern — script-opened, cross-site windows fired from any click. Real
target="_blank" links and same-site windows open as tabs. Blocked pop-ups are listed
in the address bar (open one, or always allow for the site). Limitation: allowed
pop-ups open as ordinary tabs without window.opener, so some "Sign in with…" pop-up
flows need the redirect variant.
Handing arbitrary schemes to the system is a real attack surface (smb:// makes Finder
mount an attacker's share; app URL handlers have a long history of remote-code bugs).
Schemes that reach file systems or shells (file, smb, afp, nfs, ftp, ssh,
vnc, …) are never opened from web content. Anything else needs a click in the top
frame — no subframes, no script-driven redirects — and an explicit confirmation whose
default button is Cancel.
App Sandbox (network client, user-selected files, Downloads only), hardened runtime. The app's own scripts and message handler live in an isolated content world, invisible to pages.
Frosted glass over glacier blues. The window itself is see-through (blurred, tinted
cold), the selected tab, address bar, cards and assistant are panes of real Liquid
Glass on macOS 26+ with a frosted-material fallback on older systems, and pages like
the new tab sit on a static aurora-and-glacier backdrop (no animation, no per-frame
cost). Settings → General → Appearance offers Match System, Frost (light) and
Glacier (dark); private windows use a violet frost. Palette and glass helpers live in
Sources/Support/Theme.swift.
| Setting | What it does | Cost |
|---|---|---|
| Full speed | JIT enabled | none |
| Hardened (default) | Interpreter only, extended memory tagging, dedicated processes | Content pages ~1.2× on render; pure computation ~90× |
| Lockdown | Apple's Lockdown restrictions | Some sites break |
Content sites barely notice Hardened mode because DOM work runs in native code. Compute-heavy web apps (Figma, spreadsheets, maps, games) will feel slow — give those Full speed from the shield (⌥⌘P) rather than lowering the global setting, which is the same per-site approach Microsoft reached for Edge's JIT-less mode.
The first version made The Guardian's DOMContentLoaded +470 ms slower with blocking on.
Ablations on wall-clock time (SpeedInvestigation) traced it, step by step:
| Change | Guardian DOMContentLoaded overhead |
|---|---|
Original compiler (9 lists, .* + if-domain site cosmetics) | +470 ms |
| Site-specific cosmetics scoped to the site's own document URL | +187 ms |
| Network rules in 2 lists instead of 8 (less exception duplication) | +119 ms |
Two findings drove this, both contrary to the obvious guess: 184k host-blocking rules
cost nothing measurable, while 12k site-specific cosmetic rules cost ~120 ms because a
.* trigger makes every rule a candidate on every resource load; and the number of
lists matters because each carries its own copy of the exceptions. The remaining cost
is WebKit's per-request rule matching, paid on every resource a page loads.
interactionState restores history and scroll on return. Background
tabs are CPU-throttled natively until then.youtube-adblock.js
strips the ad schedule from the player data (the uBlock Origin technique), with an
instant-skip fallback. Live A/B on three videos (YouTubeAdTests): no ad is shown or
even fetched, but YouTube holds playback back server-side, so the video starts after
~4–13 s of blank player instead of 16 s+ of ads. YouTube changes this machinery
often; that file is the one to update when ads reappear. Other uBO +js() scriptlets
(2,786 rules) are still skipped.$specifichide exceptions are approximated as $elemhide (WebKit cannot cancel
site-specific cosmetics without generic ones); uBO entity domains (site.*) are
dropped rather than guessed.WKWebExtension API that can
load Safari-style WebExtensions (the same manifest format most Chrome extensions use);
wiring that in is the realistic route to extension support and is not done yet.Download the latest disk image from
Releases, open it and
drag Icy Weasel into Applications. Builds are signed with a Developer ID and notarized
by Apple. Data lives in ~/Library/Containers/com.icyweasel.browser/.
To build and install from source instead:
scripts/fetch-phonon.sh # Read Page Aloud's voice; optional, see Building
xcodegen generate
xcodebuild -project IcyWeasel.xcodeproj -scheme IcyWeasel -configuration Release -derivedDataPath build CODE_SIGN_IDENTITY="-" build
ditto "build/Build/Products/Release/Icy Weasel.app" "/Applications/Icy Weasel.app"
scripts/release.sh direct # Developer ID: signed, notarized .dmg and .zip in build/release
scripts/release.sh appstore # Mac App Store .pkg (upload with Transporter or an API key)
One-time setup and details are at the top of scripts/release.sh. The App Store
listing text is in distribution/AppStore.md; the website is in site/.
Requires Xcode 27 and XcodeGen.
scripts/fetch-phonon.sh
xcodegen generate
xcodebuild -project IcyWeasel.xcodeproj -scheme IcyWeasel -configuration Release build
Runs on macOS 14+. Hardened JavaScript needs macOS 26.4+, native HTTPS-only 15.2+, Global Privacy Control 27; on older systems those fall back or are skipped.
scripts/fetch-phonon.sh downloads Phonon's library (51 MB) and model (163 MB) from
Gradium's pinned release into ThirdParty/Phonon, checking each against a fixed
SHA-256; they are kept out of git. Without them the app still builds and reads aloud
with Apple's voices. scripts/release.sh always fetches them.
# Offline suite: parser, compiler, WebKit integration, fingerprinting, policy
xcodebuild -project IcyWeasel.xcodeproj -scheme IcyWeasel test
# Adds live experiments: real filter lists, real trackers, real sites
TEST_RUNNER_IW_NETWORK_TESTS=1 xcodebuild -project IcyWeasel.xcodeproj -scheme IcyWeasel test
# Performance benchmarks: JavaScript speed per hardening level, user-script
# cost per frame, new-tab latency, history and suggestion costs
TEST_RUNNER_IW_PERF=1 xcodebuild -project IcyWeasel.xcodeproj -scheme IcyWeasel test \
-only-testing:IcyWeaselTests/PerformanceBenchmarks
Probe tests (RegexDialectProbe, TriggerDialectProbe, CosmeticMergeProbe,
DocumentScopedCosmeticProbe, FingerprintSurfaceProbe, HTTPSPolicyProbe) record what
the installed WebKit actually accepts, exposes and does. Several compiler decisions rest
on those observations rather than documentation; when a macOS update changes engine
behaviour, the probes are where it shows up first. SpeedInvestigation holds the
page-load ablations behind the speed numbers above.
Sources/
App/ entry point, menus, settings
Browser/ window, tabs, toolbar, shield panel, downloads, find, web view factory
Blocking/ ABP parser → layered WebKit rule compiler → cached rule-list store
ReadAloud/ Read Page Aloud: page text, Phonon engine, playback
Security/ site policy, URL sanitiser, script injection
Support/ settings, theme
Resources/
JS/ fingerprint-defense.js (page world), block-counter.js (isolated world)
Seed/ first-launch filter list
Tests/ unit, WebKit integration, probes, live experiments
Icy Weasel is licensed under the Mozilla Public License 2.0. © 2026 Mohammadreza Sepahi.
Read Page Aloud uses Phonon by Gradium: its
runtime under MIT or Apache 2.0, the Phonon 31m model and voices under Apache 2.0. The
licence texts ship inside the app with the model (Resources/PhononModel). The optional
Phonon 40m multilingual download is CC BY-NC 4.0 — non-commercial use only — and is not
covered by this repository's MPL: a commercial fork may not ship or offer it without a
licence from Gradium.
Filter lists are not bundled; the app downloads them at run time from their publishers,
and each stays under its own license (EasyList and EasyPrivacy: GPLv3 or CC BY-SA 3.0;
uBlock Origin filters: GPLv3; Peter Lowe's list and Fanboy's lists: their publishers'
terms). The bundled Public Suffix List (Resources/PublicSuffix) is © Mozilla under
MPL-2.0. Website and privacy policy: perseus-sepahi.github.io/icy-weasel.