Owlcatraz — export your Duolingo vocabulary to an Anki deck.
Owlcatraz is a Chrome (MV3) extension that reads your own learned-vocabulary list from your logged-in Duolingo session and writes it as notes into a local Anki deck via the AnkiConnect add-on. The whole flow runs in your browser: Duolingo fetch, grammatical enrichment (via the Anthropic API using your own key), and the AnkiConnect write. The data and sync layers are language-agnostic; the morphology and enrichment layers are course-specific, and the extension currently supports the Greek and French courses. Additional courses are on the roadmap.
- It does not bypass Duolingo paid features (Super, Max, Family, etc.). It only reads vocabulary you have already earned through normal study.
- It does not scrape or read other users' data. Only the signed-in user's own learned-lexemes list is read.
- It does not store, transmit, or sell any user data to any developer-controlled server. There is no Owlcatraz backend.
- It does not run automated mass requests against Duolingo's servers. It walks the user's paginated lexeme list once per sync, using the same endpoints the Duolingo website itself uses.
- It does not modify Duolingo's site behavior beyond initiating the export on the user's request.
- It does not give you a free Duolingo Super subscription, unlock premium features, or interact with billing in any way. (Worth saying explicitly — please don't ask.)
I'm a Duolingo user who also uses Anki. I wanted my vocabulary in both places — Duolingo for daily practice, Anki for spaced-repetition review on my own schedule. This tool does that for me, and now for anyone else who wants the same. The two services are complementary, not competing.
Chrome Web Store: coming soon. The extension isn't on the store yet — a listing is being prepared. Until it ships there, install it one of two ways. Both load it as an unpacked extension, so keep whichever folder you point Chrome at — Chrome reads from it on every launch, and Chrome will remind you on startup that a developer-mode extension is installed.
- Download the latest
owlcatraz-<version>.zipfrom the Releases page. - Unzip it into a folder you intend to keep (not a temporary directory).
- Open
chrome://extensionsin Chrome, toggle Developer mode on (top-right). - Click Load unpacked and select the unzipped folder (the one
containing
manifest.json).
To update later, download the newer release, replace the folder's contents, and click Reload (↻) on the extension card. The build ships with a pinned extension ID, so your settings and enrichment cache carry across updates (see the note below for the one exception — moving to the Web Store build).
- Clone the repo:
git clone https://github.com/jlopez/owlcatraz.git - Build it:
cd owlcatraz/extension pnpm install pnpm build - Open
chrome://extensionsin Chrome, toggle Developer mode on (top-right). - Click Load unpacked and select
owlcatraz/extension/dist/.
If you have the repo checked out, three pnpm scripts do the fetch-and-unpack
work for you and leave only the one-time Load unpacked click. Each
populates the same stable folder (~/.owlcatraz/unpacked), so after the first
load, updating is just the Reload (↻) click. Run them from extension/:
| Command | Installs |
|---|---|
pnpm install:latest |
The newest published GitHub release |
pnpm install:pr |
The CI preview build for the current branch's open PR |
pnpm install:local |
A fresh pnpm build of your working tree |
Chrome can't silently install a non-Web-Store extension, so the final
Load unpacked (first time) or Reload (updates) step is still manual —
the scripts open chrome://extensions, and on macOS copy the folder path to
your clipboard, to make it a few seconds. install:latest and install:pr
need the GitHub CLI (gh auth login); install:pr
detects the PR from your current branch and, if its CI is still running, waits
for the preview build to finish before loading it.
- The Owlcatraz toolbar icon appears. Click it on any tab where you are logged in to duolingo.com.
- Open Settings, paste your Anthropic API key (used for grammatical enrichment — gender, part-of-speech, inflection notes), optionally adjust the Anki deck name, and Save.
- Start Anki with the AnkiConnect add-on installed.
- Click Sync to Anki. The popup streams progress through the steps: read Duolingo session → fetch lexemes → enrich → write notes.
Re-running Sync to Anki is safe — duplicate notes are detected via a preflight pass and skipped without re-fetching audio.
When the Web Store release lands: the published build will have a different extension ID than the unpacked builds above (both of which share a pi 8000 nned ID), and Chrome scopes
chrome.storage.localper extension ID. So your settings and the grammatical-enrichment cache (the API key and the per-word LLM results) will not carry over automatically when you switch to the Store version — you'll re-enter your Anthropic API key once, and the enrichment cache rebuilds itself on the next sync (re-calling the Anthropic API for words it had previously cached). The Anki deck itself is unaffected; it lives in Anki, not in the extension.
Owlcatraz is local-first. There is no Owlcatraz server, no analytics, no telemetry, and no developer-controlled endpoint that ever receives your data.
The extension makes outbound network requests only to:
*.duolingo.com— to fetch your own profile and learned-lexemes list, using the Duolingo session cookie that's already in your browser. Same data the website shows you, fetched the same way.- Duolingo's audio CDN (
*.cloudfront.net) — to download the pronunciation audio served for each lexeme so it can be embedded in the Anki note. The exact subdomain is pinned in the manifest'shost_permissionsand lives inextension/manifest.config.ts; URLs come directly from the Duolingo API response. api.anthropic.com— to call Claude Haiku for grammatical enrichment (gender, inflection notes, English glosses). This uses your Anthropic API key, which you provide in the extension's Settings panel; calls are billed to your Anthropic account and the prompt/response data is governed by Anthropic's privacy policy. If you would rather not send your vocabulary to Anthropic, simply don't configure the key — the extension will refuse to sync until a key is present.127.0.0.1:8765— your local Anki + AnkiConnect endpoint, on loopback. Nothing leaves your machine.
The Anthropic API key, Anki deck name, and enrichment cache are stored
in chrome.storage.local — i.e. on your local Chrome profile. They are
not synced to your Google account.
If you ever want to verify any of this, the relevant code is in
extension/src/lib/ and the manifest permissions
are documented inline in
extension/manifest.config.ts.
This project is not affiliated with, endorsed by, or sponsored by Duolingo, Inc. "Duolingo" and the Duolingo owl are trademarks of Duolingo, Inc. The name Owlcatraz is an affectionate parody reference and does not imply any official relationship. The extension does not include any Duolingo logos, the owl character, or other Duolingo branding assets.
Owlcatraz uses the same authenticated session your browser already uses to talk to Duolingo's API on your behalf, walking the same lexeme-list endpoints the duolingo.com website uses to display your vocabulary.
If you are at Duolingo and have concerns about this project — whether about scope, traffic patterns, naming, or anything else — please reach out at owlcatraz@jesusla.com. I'd much rather hear from you directly than have anyone guess at intent. The project's documented scope (personal vocabulary export, narrowly construed) is the project's actual scope; the CONTRIBUTING.md file documents the out-of-scope categories that will be declined.
Issues and pull requests are welcome. Please read CONTRIBUTING.md first — it covers how to file good bug reports (Duolingo ships A/B-tested API changes constantly, so structured reports help a lot) and what categories of contribution are out of scope.
Bug reports go in Issues; open-ended questions and design conversations go in Discussions.
This project follows the Contributor Covenant.
To report a security vulnerability, please follow SECURITY.md rather than opening a public issue.
MIT © 2026 Jesus Lopez.
The remainder of this file is for contributors and curious readers. End users don't need any of it.
extension/— the Vite + TypeScript MV3 extension.fixtures/— synthesized lexeme pages and a minimal profile (~50 basic Greek words at the root, plus ~50 French words underfixtures/fr/, all drawn from general public-domain vocabulary) used to drive the 221-test offline suite..github/workflows/ci.yml— typecheck, lint, format-check, test, and build on every PR and on push tomain.
| Command | Purpose |
|---|---|
pnpm dev |
Vite build in watch mode (load unpacked from dist/) |
pnpm build |
Production build to dist/ |
pnpm test |
Run vitest once |
pnpm test:watch |
Watch-mode vitest |
pnpm typecheck |
tsc --noEmit strict typecheck |
pnpm lint |
ESLint (flat config) — fails on any warning |
pnpm format |
Prettier write |
pnpm format:check |
Prettier check (used by CI) |
The 221-test offline suite (pnpm test) covers the data, morphology,
enrichment, sync orchestration, and popup-render layers. The live
Duolingo API and AnkiConnect interactions can only be exercised against
a real Chrome profile and a running Anki + AnkiConnect. The smoke test
below assumes Greek is your active Duolingo course; a French
course works identically — the popup shows course fr and the default
deck is Duolingo::French. Smoke-test steps:
cd extension && pnpm build.- Open
chrome://extensionsin a Chrome window where you are signed in to Duolingo. Toggle Developer mode on. - Click Load unpacked and select
extension/dist/. The Owlcatraz action icon appears in the toolbar. - Click the Service worker link on the extension card. The DevTools console should be empty on a fresh load.
- Logged-out check: open an Incognito window (with extensions enabled) and click the icon. Should say "Please log in at duolingo.com".
- Confirm Greek is the active Duolingo course on duolingo.com.
- Click the icon. The popup should show your userId and "course
el". - Open Settings, paste your Anthropic API key (
sk-ant-…), optionally adjust the deck name (defaultDuolingo::Greek), Save. - Reopen the popup — the API key should round-trip back.
- Start Anki with the AnkiConnect add-on enabled.
- Click Sync to Anki. Progress should stream through five steps (session read → profile lookup → lexeme fetch → enrichment → AnkiConnect write).
- On completion, the stats table reports words fetched, enriched, added, skipped, audio stored, audio failed. In Anki, the deck contains notes with English, Target, Lemma, POS, Inflection, Notes, Audio populated; audio plays on the Recognition back side.
- Click Sync again. Second run reports everything as duplicates and finishes in seconds (the preflight skips re-fetching audio for known notes).
If anything misbehaves, check the service-worker console (it captures
syncError text plus any per-URL audio-fetch warnings) and verify the
Anthropic API key.
In rough priority order:
- Enrichment-algorithm bug fixes. A handful of known issues to track and address as they surface — the build-version auto-heal lets fixes ship without forcing users to delete and re-sync the deck.
- "Clear enrichment cache" toggle. When the LLM prompt itself changes (rather than the rendering code), bumping the build version re-renders notes from the existing cache. A user-facing toggle to drop the cache and force re-enrichment would close that gap.
- Sync progress bar. The popup currently streams text-only progress lines; a visual bar against per-step totals would be a cheap UX win for the enrichment and AnkiConnect-write steps (which have known totals up front).
- Incremental sync. A "since last sync" mode would save the Duolingo walk on daily-driver re-syncs. Lower priority because the LLM cost is already amortized by the cache and Duolingo's pagination is fast.
- Regression tests for live-only paths. Three code paths are only exercised end-to-end against a live runtime: the Origin-stripping DNR session rule, the AnkiConnect rollback-on-duplicate response shape, and the LLM-omission re-queue logic. Each is a likely candidate for silent breakage and worth a targeted boundary test.
- More courses. Greek and French are implemented as self-contained
LanguageModules underextension/src/lib/lang/; adding another course (Spanish, German, …) means a new module + fixtures + tests, with no changes to the data or sync layers. Each language picks the article convention that best teaches its gender — Greek uses the definite article, French the indefinite (une eau, not the elidedl'eau, so gender stays visible before a vowel).