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
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Tests

on:
push:
branches: [main]
pull_request:

jobs:
unit:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
- name: Shim-Dateien auf Syntaxfehler prüfen
shell: bash
run: for f in src/ff/*.js; do node --check "$f"; done
37 changes: 37 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Release

# Erstellt ein GitHub-Release, sobald ein Tag wie v0.1.0 gepusht wird.
# Das Release enthält nur dieses Repository (Build-Skript + Firefox-Shims),
# keine gebaute .xpi – die würde Code von Anthropic enthalten.
on:
push:
tags: ['v*']

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm test
- name: Release-Notizen aus CHANGELOG.md lesen
run: |
version="${GITHUB_REF_NAME#v}"
awk -v ver="$version" '
$0 ~ "^## \\[" ver "\\]" { found = 1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "Kein Abschnitt für $version in CHANGELOG.md gefunden." >&2
exit 1
fi
- name: Release erstellen
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$GITHUB_REF_NAME" --title "Claude-Firefox $GITHUB_REF_NAME" --notes-file release-notes.md --verify-tag
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
dist/
cache/
*.crx
*.zip
*.xpi
web-ext-artifacts/
node_modules/
Expand Down
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Änderungen

Alle nennenswerten Änderungen an diesem Projekt. Versionsnummern folgen [SemVer](https://semver.org/lang/de/).

## [0.1.0] – 2026-10-11

Erste Version: Die offizielle Claude-Browser-Extension („Claude in Chrome“) wird lokal auf deinem Rechner zu einem
Firefox-Add-on umgebaut. Dieses Release enthält **keinen Code von Anthropic** und keine fertige `.xpi`-Datei. Die
Extension lädt das Build-Skript beim Bauen selbst aus dem Chrome Web Store.

### So benutzt du es

1. Unten unter „Assets“ den Quellcode („Source code (zip)“) herunterladen und entpacken.
2. [Node.js](https://nodejs.org) 18 oder neuer installieren.
3. `build.cmd` doppelklicken (Windows) bzw. `node scripts/build.mjs` ausführen. Das erzeugt
`dist/claude-firefox-<version>.xpi`.
4. In Firefox `about:debugging#/runtime/this-firefox` öffnen → „Temporäres Add-on laden…“ → die `.xpi` wählen.

Details, dauerhafte Installation und Fehlerbehebung: siehe [README](README.md).

### Enthalten

- **Build-Skript** ohne Abhängigkeiten:
- CRX laden und entpacken
- Manifest für Firefox umbauen: Service Worker → Hintergrundskript, Seitenpanel → Sidebar, Berechtigungen, Add-on-ID
- Shims einbinden und Icons quadratisch machen
- XPI packen
- **Ersatz für `chrome.debugger`**:
- Screenshots
- JavaScript-Ausführung
- Klicks, Doppelklicks, Tippen (inkl. Umlaute/Emojis), Tastenkürzel, Scrollen und Drag & Drop
- Klicks in iframes
- Mitschnitt von Browser-Konsole und Netzwerk-Anfragen
- **Seitenpanel → Firefox-Sidebar** (eine Claude-Sitzung pro Fenster), echte Firefox-Tab-Gruppen
- **Anmeldung über claude.ai**: Ersatz für `externally_connectable`. Weiterleitungen auf `chrome-extension://` werden abgefangen.
- Offscreen-Dokument, Chrome-only-Optionen, `chrome://`-Links und fehlende Konstanten werden für Firefox ersetzt bzw. ergänzt.
- Windows-Hilfen: `build.cmd`, optionales Skript zur Anbindung von Claude Code / Claude Desktop (experimentell).
- Tests: Unit-Tests (`npm test`) und ein End-to-End-Test in echtem Firefox (`test/e2e/run.mjs`).

### Teststand

- Getestet mit Firefox 157 gegen die **ältere Extension-Version 1.0.70**. Alle Unit-Tests und alle End-to-End-Prüfungen bestehen.
- Vor dem Release hat eine mehrstufige Code-Review 37 Fehler gefunden, alle behoben. Die meisten betrafen Randfälle der
nachgebauten Eingaben, darunter Enter/keypress, macOS-Tastenbefehle, deaktivierte Buttons, Tab-Reihenfolge,
Pop-ups, Zoom bei Screenshots und Ruhezustand des Hintergrunds. Dazu kam eine Härtung der Login-Weiterleitung.
- **Nicht getestet:**
- die aktuelle Extension-Version
- die Anmeldung mit einem echten Claude-Konto
- die Windows-Skripte unter echtem Windows (das PowerShell-Skript ist nur auf Syntax geprüft;
die Unit-Tests laufen in der CI auch unter Windows)
- die Anbindung an Claude Code / Claude Desktop

### Bekannte Einschränkungen

- Firefox erlaubt Erweiterungen keine echten Maus- und Tastatureingaben. Der Port erzeugt die Events selbst. Nicht möglich sind:
- CSS-Hover-Menüs
- native Auswahllisten aufklappen
- Seiten, die nur „echte“ Klicks akzeptieren
- Kein Kopieren/Einfügen über die Zwischenablage (`Strg+C`/`Strg+V`).
- „Seite verlassen?“-Dialoge beim Navigieren werden nicht erkannt.
- Keine JavaScript-Ausführung auf Seiten mit strenger Content-Security-Policy.
- Keine Datei-Uploads von lokalen Pfaden.
- Im normalen Firefox nur als temporäres Add-on. Nach einem Neustart neu laden, die Anmeldung bleibt erhalten.

> Inoffizielles Projekt, nicht von Anthropic. Die Extension ist proprietäre Software von Anthropic. Nutzung nur privat
> und auf eigenes Risiko. Gebaute Dateien nicht weitergeben.
5 changes: 0 additions & 5 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ MIT License

Copyright (c) 2026 Kyloschmeilo

Diese Lizenz gilt nur für den Code in diesem Repository (Build-Skripte,
Firefox-Kompatibilitätsschicht, Tests). Die Claude-Browser-Extension selbst ist
Eigentum von Anthropic, nicht Teil dieses Repositorys und steht nicht unter
dieser Lizenz.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Expand Down
38 changes: 17 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ Chrome-Funktionen ergänzen die Skripte unter `src/ff/` zur Laufzeit.
Firefox installiert dauerhaft nur von Mozilla signierte Add-ons. Ein temporär geladenes
Add-on verschwindet beim Beenden von Firefox. Danach lädst du es über
`about:debugging#/runtime/this-firefox` neu (Tipp: Lesezeichen anlegen). Deine Anmeldung
und deine Einstellungen bleiben dabei erhalten (getestet mit Firefox 157).
und die Einstellungen der Extension bleiben dabei erhalten (getestet mit Firefox 157).
Geänderte Tastenkombinationen setzt Firefox dagegen bei jedem Neustart zurück.

> Falls die Anmeldung doch einmal verloren geht: In `about:config` die Einstellungen
> `extensions.webextensions.keepStorageOnUninstall` und
Expand All @@ -58,31 +59,23 @@ Diese Firefox-Versionen dürfen unsignierte Add-ons dauerhaft installieren:
1. `about:config` öffnen und `xpinstall.signatures.required` auf `false` setzen.
2. `about:addons` öffnen → Zahnrad → „Add-on aus Datei installieren…“ → `.xpi` wählen.

### C) Dauerhaft im normalen Firefox: selbst signieren (fortgeschritten, ungetestet)

Mozilla kann das Add-on als „nicht gelistet“ automatisch signieren:

1. Ein Konto auf [addons.mozilla.org](https://addons.mozilla.org) anlegen und unter
„Developer Hub“ → „API-Schlüssel“ einen Schlüssel erzeugen.
2. Mit eigener Add-on-ID und eigener Versionsnummer bauen:
`node scripts\build.mjs --gecko-id claude-firefox@deinname.local --version-suffix 1`
3. Signieren:
`npx web-ext sign --channel=unlisted --source-dir dist\extension --api-key=… --api-secret=…`
4. Die signierte Datei aus `web-ext-artifacts\` per Drag & Drop in Firefox ziehen.

Achtung: Dabei lädst du das Paket mit Anthropics Code zur automatischen Prüfung zu Mozilla
hoch. Jede neue Version braucht einen neuen `--version-suffix`.
> **Warum keine dauerhafte Installation im normalen Firefox?** Dafür müsste Mozilla das
> Add-on signieren. Dazu lädt man das komplette Paket – also Anthropics Code – zu
> addons.mozilla.org hoch. Das wäre eine Weitergabe an Dritte (siehe Hinweis oben) und wird
> hier deshalb nicht beschrieben.

## Bedienung

- **Claude-Symbol in der Symbolleiste:** öffnet Claude für den aktuellen Tab in der Sidebar.
- **Tastenkürzel:** dasselbe wie in Chrome (meist `Strg+E`). Kollidiert es mit Firefox, kannst
du es unter `about:addons` → Zahnrad → „Tastenkombinationen für Erweiterungen verwalten“
ändern.
ändern (bei Variante A nach jedem Firefox-Neustart erneut).
- **Pro Fenster eine Claude-Sitzung:** Die Sidebar zeigt die Sitzung des Tabs, für den du
Claude zuletzt geöffnet hast. Wechselst du den Tab, läuft eine Aufgabe weiter.
- **Updates:** `build.cmd` erneut ausführen und die neue `.xpi`-Datei laden (Variante A:
in `about:debugging` „Neu laden“ bzw. neu laden).
- **Updates:** `build.cmd` erneut ausführen. Dann in `about:debugging` über „Temporäres
Add-on laden…“ die **neue** Datei `dist\claude-firefox-<neue Version>.xpi` wählen
(Variante B: wie bei der Installation). Achtung: „Neu laden“ lädt nur die zuerst gewählte,
alte Datei erneut.

## Was funktioniert – und was nicht

Expand All @@ -91,7 +84,7 @@ hoch. Jede neue Version braucht einen neuen `--version-suffix`.
| Chat in der Sidebar | ✅ |
| Anmeldung über claude.ai | ✅ Die Anmelde-Nachricht von claude.ai bzw. die Weiterleitung auf `chrome-extension://…` wird abgefangen und an die Extension übergeben. Mit einem echten Konto noch nicht getestet. |
| Seiten lesen, Elemente finden, Formulare ausfüllen | ✅ |
| Klicken, Tippen, Tastenkürzel, Scrollen, Drag & Drop | ✅ über nachgebaute Browser-Events (Einschränkungen siehe unten) |
| Klicken, Tippen, Tastenkürzel, Scrollen, Drag & Drop | ✅ über nachgebaute Browser-Events (Einschränkungen siehe unten; Kopieren/Einfügen per Zwischenablage geht nicht) |
| Screenshots | ✅ |
| JavaScript auf der Seite ausführen | ✅ außer auf Seiten mit strenger Content-Security-Policy (Firefox lässt das nicht zu; Claude bekommt dann einen Hinweis) |
| Browser-Konsole und Netzwerk-Anfragen mitlesen | ✅ |
Expand All @@ -101,6 +94,7 @@ hoch. Jede neue Version braucht einen neuen `--version-suffix`.
| Claude Code / Claude Desktop | ⚠️ experimentell, siehe unten |
| Datei-Upload von lokalen Pfaden (aus Claude Code) | ❌ Firefox-Erweiterungen dürfen keine lokalen Dateien lesen |
| Sidebar über den Knopf auf claude.ai öffnen | ⚠️ Firefox erlaubt das Öffnen nur nach einem Klick auf das Symbol. Es erscheint ein Hinweis. |
| „Seite verlassen?“-Dialoge beim Navigieren | ⚠️ werden nicht erkannt: Claude meldet die Navigation als erfolgreich, Firefox zeigt aber seinen Dialog und bleibt auf der Seite |

**Grenzen der nachgebauten Eingaben:** Chrome lässt Claude über seine Debugger-Schnittstelle
echte Maus- und Tastatureingaben auslösen. Firefox bietet Erweiterungen das nicht an. Der
Expand All @@ -110,6 +104,8 @@ Nicht möglich sind:
- Menüs, die nur per CSS-`:hover` aufklappen
- native Auswahllisten aufklappen (Claude wählt Werte stattdessen direkt aus)
- Seiten, die ausdrücklich nur „echte“ Klicks akzeptieren (`event.isTrusted`)
- Kopieren, Ausschneiden und Einfügen über die Zwischenablage (`Strg+C`, `Strg+X`, `Strg+V`);
Claude tippt Text stattdessen direkt ein

## Claude Code / Claude Desktop anbinden (optional, experimentell)

Expand Down Expand Up @@ -161,7 +157,7 @@ node scripts/build.mjs --help
--dir <ordner> entpackte Extension, z. B. direkt aus dem Chrome-Profil:
%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions\fcoeoabgfenejglbffodgkkbkcdhcgfn
--gecko-id <id> eigene Firefox-Add-on-ID
--version-suffix <n> Versionsnummer ergänzen (für selbst signierte Builds)
--version-suffix <n> Versionsnummer um ".n" ergänzen
--keep-name Namen nicht um „(Firefox-Port)“ ergänzen
--no-xpi nur den entpackten Ordner dist/extension erzeugen
```
Expand All @@ -176,7 +172,7 @@ FIREFOX_BIN=… GECKODRIVER_BIN=… node test/e2e/run.mjs

Der End-to-End-Test installiert das gebaute Add-on in einem echten Firefox. Er ruft die
Browser-Steuerung der Original-Extension direkt auf, also denselben Code, den Claude
benutzt. Geprüft werden 44 Punkte, darunter:
benutzt. Geprüft werden unter anderem:
- Klicks, Doppelklicks und Tippen, auch mit Umlauten und Emojis
- Tastenkürzel und Formulare
- Scrollen und Klicks in fremden iframes
Expand Down
18 changes: 14 additions & 4 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Quelle (eine davon, sonst wird die aktuelle Version heruntergeladen):
Weitere Optionen:
--out <ordner> Ausgabeordner (Standard: dist)
--gecko-id <id> Firefox-Add-on-ID (Standard: claude-firefox-port@localhost)
--version-suffix <n> Hängt ".n" an die Versionsnummer an (für selbst signierte Builds)
--version-suffix <n> Hängt ".n" an die Versionsnummer an
--keep-name Namen der Extension nicht um "(Firefox-Port)" ergänzen
--no-xpi Nur den entpackten Ordner erzeugen
-h, --help Diese Hilfe
Expand Down Expand Up @@ -191,7 +191,6 @@ export async function build(opts = {}) {
console.log(`• ${chromeManifest.name} ${chromeManifest.version} (Chrome-ID ${chromeExtensionId})`);

files.set('manifest.json', Buffer.from(JSON.stringify(manifest, null, 2) + '\n'));
config.pages = [...files.keys()].filter((n) => n.endsWith('.html')).sort();

// Mozilla verlangt quadratische Icons (das Original-Icon ist es nicht immer).
const iconPaths = new Set();
Expand All @@ -211,9 +210,11 @@ export async function build(opts = {}) {
}
}

// Sandbox-Seiten haben keine Extension-APIs – dort keine Shims einfügen.
const sandboxPages = new Set(config.sandboxPages.map((p) => p.replace(/^\//, '')));
let pages = 0;
for (const [name, data] of files) {
if (name.endsWith('.html')) {
if (name.endsWith('.html') && !sandboxPages.has(name)) {
files.set(name, Buffer.from(injectShims(data.toString('utf8'), PAGE_SHIMS)));
pages++;
}
Expand Down Expand Up @@ -277,7 +278,16 @@ async function main() {
console.log('\nFertig. Installation in Firefox: siehe README.md, Abschnitt "Installation".');
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
// Auch über Symlinks/Junctions (Windows) als Hauptskript erkennen.
function isMainModule() {
try {
return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}

if (isMainModule()) {
main().catch((err) => {
console.error(`\nFehler: ${err.message}`);
process.exitCode = 1;
Expand Down
9 changes: 7 additions & 2 deletions scripts/lib/manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ const DROPPED_KEYS = [
'differential_fingerprint',
'side_panel',
'externally_connectable',
'sandbox',
'oauth2',
'export',
'import',
Expand Down Expand Up @@ -280,7 +279,12 @@ export function transformManifest(chromeManifest, options = {}) {

// --- Content-Security-Policy ---------------------------------------------
if (isPlainObject(m.content_security_policy)) {
delete m.content_security_policy.sandbox;
// Firefox akzeptiert eine Sandbox-CSP nur mit "sandbox"-Direktive und ohne allow-same-origin.
const sb = m.content_security_policy.sandbox;
if (sb !== undefined && (typeof sb !== 'string' || !/(^|;)\s*sandbox(\s|;|$)/.test(sb) || /allow-same-origin/.test(sb))) {
delete m.content_security_policy.sandbox;
report.removedKeys.push('content_security_policy.sandbox');
}
if (!Object.keys(m.content_security_policy).length) delete m.content_security_policy;
} else if (typeof m.content_security_policy === 'string') {
m.content_security_policy = { extension_pages: m.content_security_policy };
Expand All @@ -297,6 +301,7 @@ export function transformManifest(chromeManifest, options = {}) {
externallyConnectable,
sidePanelPath,
sidePanelCommands,
sandboxPages: Array.isArray(m.sandbox?.pages) ? m.sandbox.pages.filter((p) => typeof p === 'string') : [],
};

return { manifest: m, config, report };
Expand Down
3 changes: 2 additions & 1 deletion scripts/lib/zip.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export function readZip(buf) {
const name = buf.toString(flags & 0x0800 ? 'utf8' : 'latin1', p + 46, p + 46 + nameLen);
p += 46 + nameLen + extraLen + commentLen;

if (name.endsWith('/')) continue;
// Ordner-Einträge überspringen (auch "ordner\" aus Windows-ZIPs).
if (name.replace(/\\/g, '/').endsWith('/')) continue;
if (flags & 0x1) throw new Error(`Verschlüsselte ZIP-Einträge werden nicht unterstützt: ${name}`);
if (buf.readUInt32LE(localOffset) !== SIG_LOCAL) throw new Error(`Beschädigtes ZIP (Local Header für ${name}).`);
const localNameLen = buf.readUInt16LE(localOffset + 26);
Expand Down
5 changes: 3 additions & 2 deletions scripts/windows/install-native-host.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ foreach ($name in $hostNames) {
if ($chromeManifest) { break }
}
}
if (-not $chromeManifest -or -not (Test-Path $chromeManifest)) {
if (-not $chromeManifest -or -not (Test-Path -LiteralPath $chromeManifest)) {
Write-Host " - $name : keine Chrome-Registrierung gefunden (Programm nicht installiert oder nicht für Chrome eingerichtet)."
continue
}

$json = Get-Content -Raw -Path $chromeManifest | ConvertFrom-Json
# Claude Code/Desktop schreiben UTF-8 ohne BOM; Windows PowerShell 5.1 läse sonst ANSI.
$json = Get-Content -Raw -Encoding UTF8 -LiteralPath $chromeManifest | ConvertFrom-Json
$hostPath = [string]$json.path
if (-not [System.IO.Path]::IsPathRooted($hostPath)) {
$hostPath = Join-Path (Split-Path -Parent $chromeManifest) $hostPath
Expand Down
23 changes: 13 additions & 10 deletions src/ff/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,17 +295,20 @@
if (handledRedirects.has(url)) return;
handledRedirects.set(url, now);

const path = (m[2] || '/').replace(/^\//, '');
const rest = url.slice(m[0].length);
const pageUrl = api.runtime.getURL(path) + rest;
// Gibt es die Zielseite in der Extension, laden wir sie (wie Chrome es täte).
const exists = !!path && (config.pages || []).includes(path);
if (exists && tab && tab.id != null) {
ff.lastRedirect = { url, at: now, via: 'page' };
await api.tabs.update(tab.id, { url: pageUrl }).catch((err) => ff.warn(err));
// Nur Weiterleitungen zu einer Anmeldung, die die Extension selbst gestartet
// hat: Der state-Parameter muss zum gespeicherten oauthState passen. So kann
// keine fremde Seite einen eigenen Login-Code unterschieben.
let state = null;
try {
state = new URL(url).searchParams.get('state');
} catch {}
const { oauthState } = await api.storage.local.get('oauthState');
if (!state || state !== oauthState) {
ff.lastRedirect = { url, at: now, via: 'ignored', reason: 'state' };
ff.warn('OAuth-Weiterleitung ignoriert: state passt nicht zur gestarteten Anmeldung.');
return;
}
// Sonst so tun, als hätte claude.ai die Weiterleitung per Nachricht geschickt.
// So behandeln, als hätte claude.ai die Weiterleitung per Nachricht geschickt.
const sender = { url: tab && tab.url, origin, tab, frameId: 0, documentLifecycle: 'active' };
const result = await dispatchExternal({ type: 'oauth_redirect', redirect_uri: url }, sender);
ff.lastRedirect = { url, at: now, via: 'message', result };
Expand All @@ -326,7 +329,7 @@
.then((tab) => handleRedirect(d.redirectUrl, tab, origin))
.catch(() => handleRedirect(d.redirectUrl, { id: d.tabId }, origin));
},
{ urls: config.externallyConnectable, types: ['main_frame', 'sub_frame'] },
{ urls: config.externallyConnectable, types: ['main_frame'] },
);
} catch (err) {
ff.warn('webRequest.onBeforeRedirect nicht verfügbar:', err);
Expand Down
Loading
Loading