Open-source project
duckduckgo/duckduckgo-privacy-extension avatar
duckduckgo/duckduckgo-privacy-extension

DuckDuckGo Privacy Extensions block tracking from inside the browser

DuckDuckGo Privacy Essentials browser extension for Firefox, Chrome.

1,505 stars337 forksJavaScriptApache-2.0

At a glance

What is it?
The DuckDuckGo privacy extension codebase is the JavaScript behind a browser extension distributed for Firefox, Chrome, Edge and Opera, built on a shared core with per browser bundles and a shared tracker lookup table. It is Apache-2.0 licensed, ships dated releases, and is explicitly not accepting new feature pull requests.
Who is it for?
DuckDuckGo Privacy Extensions fit a user who wants blocking at the browser level without a proxy or a VPN, and a developer who wants to read how a production tracking blocker is put together, since the shared core, the per browser build matrix and the anonymous site reporting flow are all in this repository.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One shared core, four browser builds

The repository holds the DuckDuckGo Firefox, Chrome and Edge extensions, with links to the store listings for Firefox, Chrome, Edge and Opera, and a separate repository for the Safari version, which is an Xcode project distributed through the App Store.

The layout shows the architecture before the code does. There is a shared/ directory, a browsers/ directory, a packages/ directory, a types/ directory, a docs/ directory and a scripts/ directory, plus separate test directories for unit tests, integration tests and a Playwright setup.

The shared directory is the core. The browsers directory holds the per browser manifests and platform specific glue, and the Makefile makes the split explicit. The browser variable selects the target, and for anything other than firefox the CSS platform name becomes chrome-mv3 and the browser type is set to chrome, which is how one codebase serves a Manifest V3 Chromium build and a Firefox build with different manifest formats.

make
BROWSER_TYPE = $(browser)
CSS_PLATFORM_NAME = $(browser)
ifneq ('$(browser)','firefox')
  CSS_PLATFORM_NAME = chrome-mv3
  BROWSER_TYPE = chrome
endif

The build output path follows the same convention, build/$(browser)/$(type), with a special case for the test type. The release target cleans, installs, copies and builds, and the chrome-beta and beta-firefox targets wrap it and produce a zip.

The Makefile also handles the fact that some source lives in other packages. The watched files list is built by finding files in browsers/, shared/, packages/ and unit-test/, and it conditionally adds the contents of node_modules/@duckduckgo/ if that directory exists, so that files arriving from a published dependency are included in the watch and the bundle.

The tracker lookup table is generated, not hand written

One npm script is worth reading on its own, because it explains where the extension's blocking data comes from.

json
"bundle-config": "node scripts/bundleConfig.mjs && node node_modules/@duckduckgo/content-scope-scripts/injected/scripts/bundleTrackers.mjs > shared/data/bundled/tracker-lookup.json"

The first step runs a project script, and the second runs a script from the @duckduckgo/content-scope-scripts package, redirecting its output into shared/data/bundled/tracker-lookup.json. So the tracker lookup table is generated at build time from the content scope scripts package, and it lands in the shared data directory where the rest of the code can read it.

That is a meaningful architectural choice for a blocking extension. The list of trackers is the product, and generating it from a maintained package means the list can be updated on a cadence that is independent of the extension release cycle, and that several consumers, the browser extension and the other DuckDuckGo apps, share one source of truth.

The Makefile's conditional inclusion of node_modules/@duckduckgo/ files supports the same idea. It is not only the extension's own code that gets bundled, but the code that arrives as a dependency, and the build watches both.

The content-scope-scripts package is also where the script injection lives, since the directory name injected/scripts is part of the path used above. The extension works by rewriting page requests, so the rules that decide what to rewrite have to arrive as data or as injected script, and the build treats them as a first class input rather than as a static file checked into the repository.

Three test layers and a type check that gates the lint task

The test setup is broader than most extension projects manage, and the package.json shows how the pieces fit.

json
"test": "npm run test.unit && npm run test.node",
"test.unit": "make unit-test",
"test.node": "make node-test",
"lint": "npm run check-lockfile && npm run eslint && npm run prettier && npm run tsc"

There is a unit test layer, a node test layer, and a Playwright layer for browser testing, with separate configurations for the two browser families and one more for a chromium embedded build.

json
"playwright-firefox": "make dev browser=firefox type=dev && playwright test --config=playwright.firefox.config.js",
"playwright-chromium-embedded": "make dev browser=chromium-embedded type=dev && PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 playwright test --config=playwright.chromium-embedded.config.js"

The chromium embedded configuration sets an experimental Playwright flag for service worker network events, which is a strong hint about the hardest part of testing this kind of extension: in a Manifest V3 extension, the request interception happens in a service worker, and observing it from a test harness needs the experimental network event API. The karma.conf.js at the root is a second unit test runner, which is the older path, and the pixel-definitions/ directory holds the image assets used to verify the extension's own interface pixel by pixel.

The lint task is a chain of four checks, and the ordering is deliberate. check-lockfile runs first, so a stale lockfile is caught before the linters run, then eslint, then prettier in check mode, then tsc. The eslint invocation is scoped to a fixed list of paths, shared/js, shared/data, unit-test, integration-test and scripts, which is a narrower scope than the whole tree and a sign that the root-level configuration files are handled separately.

The dev script is a small honesty gesture.

json
"dev": "echo 'Try npm run dev-firefox or npm run dev-chrome instead' && exit 0"

A generic dev command would be ambiguous in a project with a browser matrix, so it prints a pointer and exits successfully rather than doing something half useful.

Reporting a broken site is an anonymous link in the popup

A blocking extension has a category of bug that no other extension has, the site that breaks. The README addresses it with a feature rather than an issue tracker.

Report broken sites using the anonymous Report Broken Site link in the extension popup.

The word anonymous is doing work there, and it is a deliberate design decision. A user whose site is broken by a blocking rule is likely to be a site owner, and asking them to file a GitHub issue with a reproduction case would require a GitHub account and would identify them. A popup link that reports anonymously removes both barriers, which means the project gets reports from the people most likely to know that a rule is wrong.

The alternative design, and the one that fails more often, is to only accept reports from within the community that already files issues. That loses the site owner entirely, and the site owner is the person who can say which script on which path is being blocked.

The second feedback channel is the ordinary bug report path, and the README points at CONTRIBUTING.md for the reporting bugs section. The distinction matters because the two routes have different expectations. An anonymous broken site report is a data point for the rule set, while a bug report is a reproducible defect in the extension code, and the project keeps them in separate places rather than mixing them into one issue queue.

For the protection behaviour itself, the README directs readers to the Web Tracking Protections help page, which explains how each protection works and how they overlap across the extension and the apps.

The project accepts bug fixes, not features

The Development section of the README contains the single most consequential sentence in the document for anyone planning to use this as a base.

We're not accepting new feature pull requests right now. For bug fixes see Development in CONTRIBUTING.md.

That is a product decision with a clear reason behind it, even if the reason is not stated in the README. A privacy extension's value is in its rules and its correctness, and both change the product's behaviour for every user the moment they ship. Accepting a feature contribution means accepting a change to what gets blocked, which is a much harder review than accepting a fix to code that is already wrong.

The practical consequences are worth being clear about. If you were planning to add a blocking rule, a new protection or a new interface element, this repository is not the place to do it without a prior conversation. If you were planning to fix a broken site rule or a bug in the injection code, that is welcomed, and CONTRIBUTING.md has the development instructions.

The maintenance picture supports the same reading. The releases are dated by day rather than by semantic version, with 2026.8.6 on 2026-08-06, 2026.8.24 on 2026-08-24 and 2026.10.6 on 2026-10-07, and the last push to main was on 2026-10-16. A dated release scheme is what you would expect from a product that ships a rule set update on the same cadence as a code change, since the date tells you exactly how fresh the blocking data is.

The repository is Apache-2.0 licensed with a LICENSE.md at the root, and the extension requires Node 20 or newer with npm 10 or newer, so the build environment is a recent one.

What this codebase is not

The first limitation is scope. This is browser level blocking, not network level filtering. It protects the browser you install it in, and it does not protect the other applications on your machine, your DNS queries, or the traffic of a device on your network. If the requirement is a whole network, the answer is a different product category, and the extension cannot be the answer to it.

The second limitation is that the tracker list is one input among several, and the others are not in this repository. The generated tracker-lookup.json comes from the content-scope-scripts package, and the code that is injected into pages comes from the same place. If you want to understand or modify how a specific category of tracker is detected, you are reading a package that this repository depends on, not this repository.

The third limitation is the Safari gap. The README is explicit that the Safari extension code is an Xcode project in a different repository, privacy-essentials-safari. So the multi browser coverage is not as uniform as the store links suggest, and anyone working on the Safari build is working in a different codebase with a different toolchain and a different release process.

The fourth limitation is what a blocked site looks like. A blocking extension that breaks a site produces a support burden, and the anonymous reporting link is a mitigation rather than a fix. The set of rules will always be a compromise between coverage and breakage, and a project that is not accepting feature pull requests is choosing to refine the existing rules rather than expand them.

Editorial conclusion

DuckDuckGo Privacy Extensions fit a user who wants blocking at the browser level without a proxy or a VPN, and a developer who wants to read how a production tracking blocker is put together, since the shared core, the per browser build matrix and the anonymous site reporting flow are all in this repository. It does not fit a team looking for network level filtering, and it does not fit anyone planning to extend the product, because the project states it is not accepting new feature pull requests. Before contributing, read CONTRIBUTING.md, note that bug fixes are the accepted form of contribution, and check the dated release tags, with 2026.10.6 from 2026-10-07 as the most recent, against the last push on 2026-10-16.

Frequently asked questions

Which browsers does the DuckDuckGo privacy extension support?

The repository holds the Firefox, Chrome and Edge extensions, with store links for Firefox, Chrome, Edge and Opera, and a note that other Chromium-based browsers such as Vivaldi can usually install the Chrome version. The Safari extension is a separate Xcode project in the privacy-essentials-safari repository, distributed through the App Store.

How do I build the DuckDuckGo privacy extension locally?

The npm scripts offer dev-firefox and dev-chrome, which run make watch with the browser and type set, and open-dev-firefox which runs web-ext against build/firefox/dev. The release targets are make release with browser and type set, plus chrome-beta and beta-firefox targets that also produce a zip. The build requires Node 20 or newer and npm 10 or newer.

How do I report a site that the extension breaks?

Use the anonymous Report Broken Site link in the extension popup. The README describes it as an anonymous link, and it is separate from the ordinary bug reporting process, which is documented in CONTRIBUTING.md.

Can I contribute a new feature to the DuckDuckGo privacy extension?

No. The README states that the project is not accepting new feature pull requests right now, and points contributors to the Development section of CONTRIBUTING.md for bug fixes. The extension is licensed under Apache 2.0 with a LICENSE.md file in the repository.

How does the extension get its list of trackers?

The tracker lookup table is generated at build time. The bundle-config script runs scripts/bundleConfig.mjs and then runs bundleTrackers.mjs from the @duckduckgo/content-scope-scripts package, redirecting the output into shared/data/bundled/tracker-lookup.json.

Official sources

  1. duckduckgo/duckduckgo-privacy-extension on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/duckduckgo-duckduckgo-privacy-extension.svg)](https://hysenlabs.com/projects/duckduckgo-duckduckgo-privacy-extension)