Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Phi.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
D1F600042FD6000000000004 /* DiffableOutlineDiffPlanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F600032FD6000000000003 /* DiffableOutlineDiffPlanner.swift */; };
D1F600062FD6000000000006 /* DiffableOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F600052FD6000000000005 /* DiffableOutlineView.swift */; };
D1F600082FD6000000000008 /* SidebarDiffableSnapshotBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F600072FD6000000000007 /* SidebarDiffableSnapshotBuilder.swift */; };
D7A000022FE7000000000002 /* TrackingProtection in Resources */ = {isa = PBXBuildFile; fileRef = D7A000012FE7000000000001 /* TrackingProtection */; };
DEBE00000000000000000005 /* DeveloperSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE00000000000000000002 /* DeveloperSettingViewController.swift */; };
DEBE00000000000000000006 /* DeveloperSettingHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE00000000000000000003 /* DeveloperSettingHostingViewController.swift */; };
DEBE00000000000000000007 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE00000000000000000004 /* DeveloperSettingsView.swift */; };
Expand Down Expand Up @@ -856,6 +857,7 @@
D1F600032FD6000000000003 /* DiffableOutlineDiffPlanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableOutlineDiffPlanner.swift; sourceTree = "<group>"; };
D1F600052FD6000000000005 /* DiffableOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableOutlineView.swift; sourceTree = "<group>"; };
D1F600072FD6000000000007 /* SidebarDiffableSnapshotBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarDiffableSnapshotBuilder.swift; sourceTree = "<group>"; };
D7A000012FE7000000000001 /* TrackingProtection */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TrackingProtection; sourceTree = "<group>"; };
DEBE00000000000000000002 /* DeveloperSettingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingViewController.swift; sourceTree = "<group>"; };
DEBE00000000000000000003 /* DeveloperSettingHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingHostingViewController.swift; sourceTree = "<group>"; };
DEBE00000000000000000004 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1528,6 +1530,7 @@
B0C469752E3B158D007F0C7A /* Resources */ = {
isa = PBXGroup;
children = (
D7A000012FE7000000000001 /* TrackingProtection */,
8EC502142F67AF7F00CE7338 /* DownloadSafety.xcstrings */,
375C66372EFB923C00885D78 /* Fonts */,
3765817A2EE9637F00D3E031 /* Localizable.xcstrings */,
Expand Down Expand Up @@ -2219,6 +2222,7 @@
buildActionMask = 2147483647;
files = (
CDA11ED500000000000000B1 /* claude-skill in Resources */,
D7A000022FE7000000000002 /* TrackingProtection in Resources */,
37F574482F7A7C5D00879867 /* LuaIcon.icon in Resources */,
B0C4694E2E3B10E2007F0C7A /* Assets.xcassets in Resources */,
3765817B2EE963AD00D3E031 /* Localizable.xcstrings in Resources */,
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ focuses on the core browsing experience.
- Native macOS windows, sidebars, settings, and system integration
- Chromium tabs, profiles, extensions, downloads, bookmarks, and DevTools
- Spaces for organizing tabs and profiles
- Built-in local tracking-parameter protection with Remove and Mask modes
- An optional local CDP skill for supervised browser automation

Inherited Phi account, connector, rollback, crash-help, and update services
Expand Down Expand Up @@ -86,6 +87,23 @@ The old `phi://` URL scheme remains accepted temporarily alongside canonical
`lua://` links. macOS permissions and keychain items tied to the former bundle
identifier may require reapproval.

## Tracking protection

Lua bundles a local Chromium extension that recognizes common advertising and
email attribution parameters such as `gclid`, `fbclid`, `msclkid`, and
`utm_*`. Remove mode strips them; Mask mode retains the parameter names while
replacing their values. The extension can be paused and keeps a per-profile
counter in Chromium extension storage. It has no server, account, or telemetry.
The feature was inspired by the public
[`donttrackme`](https://github.com/kiwi-init/donttrackme) project and was
implemented for Chromium as source-owned Lua code.

Followed links are cleaned before their destination navigation. When a tracked
URL is pasted, opened externally, or reached through a redirect, the initial
request may already contain its parameters; Lua then cleans the visible URL in
place. This is URL hygiene, not a network request blocker or a substitute for
engine-level anti-fingerprinting.

## Contributing and releases

Open pull requests against `main`. Releases are immutable `vX.Y.Z` tags from
Expand Down
25 changes: 25 additions & 0 deletions Resources/TrackingProtection/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2026 Phinomenon Inc.
// Licensed under the Apache License, Version 2.0.

"use strict";

let counterUpdate = Promise.resolve();

chrome.runtime.onInstalled.addListener(() => {
chrome.storage.local.get({ mode: "remove", enabled: true, cleaned: 0 }, state => {
chrome.storage.local.set(state);
});
});

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type !== "trackers-cleaned") return false;
const increment = Number.isSafeInteger(message.count) && message.count > 0 ? message.count : 0;
if (increment === 0) return false;

counterUpdate = counterUpdate
.then(() => chrome.storage.local.get({ cleaned: 0 }))
.then(state => chrome.storage.local.set({ cleaned: state.cleaned + increment }))
.then(() => sendResponse({ ok: true }))
.catch(() => sendResponse({ ok: false }));
return true;
});
69 changes: 69 additions & 0 deletions Resources/TrackingProtection/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2026 Phinomenon Inc.
// Licensed under the Apache License, Version 2.0.

(() => {
"use strict";

let mode = "remove";
let enabled = true;

function reportCleaned(count) {
if (count <= 0) return;
chrome.runtime.sendMessage({ type: "trackers-cleaned", count }, () => {
// Reading lastError suppresses noise when the extension is reloaded
// while a page still owns the previous content-script context.
void chrome.runtime.lastError;
});
}

function cleanCurrentURL() {
if (!enabled) return;
const result = LuaTrackingProtection.cleanURL(location.href, mode);
if (!result || result.url === location.href) return;

try {
history.replaceState(history.state, "", result.url);
reportCleaned(result.count);
} catch {
// Some documents disallow history mutation. Link cleaning remains active.
}
}

function cleanAnchor(anchor) {
if (!enabled || !anchor?.href) return;
const result = LuaTrackingProtection.cleanURL(anchor.href, mode);
if (!result || result.url === anchor.href) return;
anchor.href = result.url;
reportCleaned(result.count);
}

function anchorFromEvent(event) {
const target = event.composedPath?.()[0] ?? event.target;
return target?.closest?.("a[href]") ?? null;
}

for (const eventName of ["pointerdown", "mousedown", "auxclick", "click", "contextmenu"]) {
document.addEventListener(eventName, event => cleanAnchor(anchorFromEvent(event)), true);
}

document.addEventListener("keydown", event => {
if (event.key === "Enter") cleanAnchor(anchorFromEvent(event));
}, true);

for (const eventName of ["pageshow", "popstate", "hashchange"]) {
addEventListener(eventName, cleanCurrentURL, true);
}

chrome.storage.local.get({ mode: "remove", enabled: true }, state => {
mode = state.mode === "mask" ? "mask" : "remove";
enabled = state.enabled !== false;
cleanCurrentURL();
});

chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== "local") return;
if (changes.mode) mode = changes.mode.newValue === "mask" ? "mask" : "remove";
if (changes.enabled) enabled = changes.enabled.newValue !== false;
cleanCurrentURL();
});
})();
24 changes: 24 additions & 0 deletions Resources/TrackingProtection/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"manifest_version": 3,
"name": "Lua Tracking Protection",
"description": "Removes or masks known tracking parameters before followed links navigate.",
"version": "1.0.0",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsfzMxXZotLCRJbx4JtRSfcxk9hm22o+ZkoCoJLcR5SDhv2nWQ5yguFQHo5F674bFHvoXa1CbubYVhRHwEK5g34dhQKc9kLIfczvieA7tIzzv5yUZ+gAX/cawfXotJCaZE8A2BpXseBsGeb3ler5zAR0ems0s6iqX0pXmB/KztZ+AtpnTAR8YEur0XY+yQtscVURCCdCfxfuhhhHZKC826+VjMVGQBVTSbBOSuuRqZoBIzbV9Pxf42wFcq8O5C3Go6MbdxZVenCNEPwPIR0UeJU7qU9omDJiq1gALtIEd+8A9EOa7JCg+uQcVkRlUTxyht1jm9qsRyZjUZ2WYDCNCPQIDAQAB",
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["url-cleaner.js", "content.js"],
"run_at": "document_start",
"all_frames": true
}
],
"action": {
"default_title": "Lua Tracking Protection",
"default_popup": "popup.html"
},
"permissions": ["storage"],
"host_permissions": ["<all_urls>"]
}
87 changes: 87 additions & 0 deletions Resources/TrackingProtection/popup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* Copyright 2026 Phinomenon Inc. Licensed under Apache-2.0. */

:root {
color-scheme: light dark;
--background: #ffffff;
--surface: #f3f3f5;
--text: #171719;
--secondary: #6c6c72;
--border: #dedee2;
--accent: #71717a;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #171719;
--surface: #262629;
--text: #f5f5f6;
--secondary: #a1a1a7;
--border: #38383d;
--accent: #a1a1aa;
}
}

* { box-sizing: border-box; }

body {
width: 310px;
margin: 0;
padding: 16px;
background: var(--background);
color: var(--text);
font: 13px -apple-system, BlinkMacSystemFont, sans-serif;
}

header, .mode {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}

header div { display: grid; gap: 3px; }
header strong { font-size: 14px; }
header div span, #status, .count span, .mode > span { color: var(--secondary); }
header div span { font-size: 11px; }

.switch { position: relative; width: 36px; height: 22px; flex: 0 0 auto; }
.switch input { position: absolute; opacity: 0; }
.switch span {
position: absolute;
inset: 0;
border-radius: 11px;
background: var(--border);
transition: background 120ms ease;
}
.switch span::after {
content: "";
position: absolute;
width: 18px;
height: 18px;
top: 2px;
left: 2px;
border-radius: 50%;
background: white;
box-shadow: 0 1px 3px rgb(0 0 0 / 22%);
transition: transform 120ms ease;
}
.switch input:checked + span { background: var(--accent); }
.switch input:checked + span::after { transform: translateX(14px); }

main { display: grid; gap: 12px; margin-top: 14px; }
.count { display: grid; place-items: center; gap: 3px; padding: 14px; border-radius: 10px; background: var(--surface); }
.count strong { font-size: 28px; line-height: 1; }
.count span { font-size: 10px; letter-spacing: .04em; text-transform: uppercase; }

.mode div { display: flex; padding: 2px; border-radius: 8px; background: var(--surface); }
button {
border: 0;
border-radius: 6px;
padding: 6px 13px;
background: transparent;
color: var(--text);
font: inherit;
cursor: pointer;
}
button.selected { background: var(--accent); color: white; }
#status { min-height: 32px; margin: 0; font-size: 11px; line-height: 1.45; text-align: center; }
40 changes: 40 additions & 0 deletions Resources/TrackingProtection/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!doctype html>
<!-- Copyright 2026 Phinomenon Inc. Licensed under Apache-2.0. -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lua Tracking Protection</title>
<link rel="stylesheet" href="popup.css">
<script src="popup.js" defer></script>
</head>
<body>
<header>
<div>
<strong>Tracking Protection</strong>
<span>Clean tracking parameters from URLs</span>
</div>
<label class="switch" aria-label="Enable tracking protection">
<input id="enabled" type="checkbox">
<span></span>
</label>
</header>

<main>
<section class="count">
<strong id="cleaned">0</strong>
<span>parameters cleaned</span>
</section>

<section class="mode">
<span>Mode</span>
<div role="group" aria-label="Tracking protection mode">
<button id="remove" type="button">Remove</button>
<button id="mask" type="button">Mask</button>
</div>
</section>

<p id="status"></p>
</main>
</body>
</html>
42 changes: 42 additions & 0 deletions Resources/TrackingProtection/popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2026 Phinomenon Inc.
// Licensed under the Apache License, Version 2.0.

"use strict";

const enabledControl = document.querySelector("#enabled");
const cleanedLabel = document.querySelector("#cleaned");
const removeButton = document.querySelector("#remove");
const maskButton = document.querySelector("#mask");
const statusLabel = document.querySelector("#status");

function render(state) {
const mode = state.mode === "mask" ? "mask" : "remove";
const enabled = state.enabled !== false;
enabledControl.checked = enabled;
cleanedLabel.textContent = String(Number.isSafeInteger(state.cleaned) ? state.cleaned : 0);
removeButton.classList.toggle("selected", mode === "remove");
maskButton.classList.toggle("selected", mode === "mask");

if (!enabled) {
statusLabel.textContent = "Paused. Tracking parameters are left unchanged.";
} else if (mode === "remove") {
statusLabel.textContent = "Known tracking parameters are removed from followed links.";
} else {
statusLabel.textContent = "Known tracking values are replaced with random tokens.";
}
}

function readState() {
chrome.storage.local.get({ mode: "remove", enabled: true, cleaned: 0 }, render);
}

enabledControl.addEventListener("change", () => {
chrome.storage.local.set({ enabled: enabledControl.checked });
});
removeButton.addEventListener("click", () => chrome.storage.local.set({ mode: "remove" }));
maskButton.addEventListener("click", () => chrome.storage.local.set({ mode: "mask" }));
chrome.storage.onChanged.addListener((_changes, areaName) => {
if (areaName === "local") readState();
});

readState();
Loading
Loading