This is the Hacktoolkit Chrome extension skeleton. It ships as a small working extension, Tab Counter, so every piece can be run and tested before you change anything. To start your own extension, see Using this skeleton.
Counts your tabs and closes the blank ones. Manifest V3, vanilla JavaScript, no build step, no dependencies. Works in Chrome, Brave, Edge and other Chromium browsers (Chrome 121 or newer).
- Create a repository from this template on GitHub (Use this template), or clone it.
- Run the init script once. It renames everything, resets the version, and deletes itself:
node scripts/init.mjs "My Extension" "What it does in one line" hacktoolkit/my-extension
- Replace
img/icon*.pngwith your icon at 16, 32, 48 and 128 pixels. - Put your logic in
src/lib/as pure functions with tests, and your actions insrc/lib/actions.js. make test,make e2e,make dev.
manifest.json Manifest V3, module service worker, popup, options page, commands
src/background.js service worker: message router, context menu, shortcuts, badge
src/lib/logic.js pure logic, no Chrome APIs, unit tested
src/lib/settings.js chrome.storage.sync with local fallback, export/import
src/lib/actions.js the action registry: id, name, help, preview(), run()
src/popup.* toolbar popup that only sends messages
src/options.* settings page
src/ui.css design tokens, light and dark
test/ node --test suites
scripts/e2e.mjs headless browser test over the DevTools protocol
scripts/dev.mjs the holodeck: a throwaway dev profile with sample tabs
scripts/init.mjs one-time rename
Makefile help, test, check, e2e, dev, dev-clean, package
.github/workflows CI: unit tests on Node 22 and 24, headless e2e, zip artifact
AGENTS.md rules of the road for contributors and AI agents (CLAUDE.md imports it)
- All browser operations run in the service worker. The popup sends
chrome.runtime.sendMessageand renders what comes back. Chrome closes the popup the moment a window is created or focused, so logic that lives there dies mid-action. - Actions are data. Each entry in
ACTIONShas anid,name,help, optionalpreview()returning{ title, items, confirmLabel }, andrun()returning{ message }. The popup, context menu, keyboard commands and e2e harness all consume the same registry. - Pure logic is separate and tested. Anything expressible as plain functions over plain data goes in
src/lib/logic.jswith anode --testcase. - Settings sync.
src/lib/settings.jssaves tochrome.storage.sync, falls back to local, picks the newer copy on load, and exports and imports JSON.
make test # unit tests, no browser needed
make check # syntax-check every script
make e2e # every action in a real headless browser
make dev # interactive: the holodeck
make package # <name>-<version>.zip for the Chrome Web StoreBrowser testing never touches a real profile. make dev opens Brave (or BROWSER=/path/to/chromium make dev) on a throwaway profile at ~/.holodeck/<browser>/, named "🧪 holodeck" in the profile chip, with the extension loaded unpacked and sample tabs. The profile persists between runs; make dev-clean deletes it. make e2e uses a fresh profile under ~/.holodeck/tmp/ and removes it afterwards. Google Chrome's branded build ignores --load-extension, so the scripts default to Brave; CI installs Chrome for Testing.
Loading unpacked into your real profile: chrome://extensions, Developer mode, Load unpacked, pick this folder.
Bump version in manifest.json and package.json, make package, upload the zip to the Chrome Web Store dashboard. CI attaches the same zip to every run.
MIT. See LICENSE.md.