All projects
08WEB APP2026
Site Lock extension icon: a cyan blob with a green outline and a white globe glyph at its centre

Site Lock

A Manifest V3 browser extension that puts a master password in front of the sites that steal your focus — rules, hashing and history all stay on the device.

Get it on the Chrome Web StoreChrome · Manifest V3
Chrome Web Store
Site Lock options page listing three protected sites with Password Lock, Fake Loader and Redirect modes

Overview

What is Site Lock

Site Lock is a Manifest V3 browser extension that puts a master password in front of the sites that steal your focus. Five block modes per site — a password overlay, a redirect, closing the tab, a fake loader or a convincing error page — with rules, the bcrypt-hashed password and all history kept in the browser profile. No server anywhere.

Role
Creator & maintainer — designed and built the extension, the marketing site and the test suites.
Platform
Chrome · Manifest V3
Stack
TypeScript · React · Vite · CRXJS · bcryptjs
Tests
44 unit · 28 Playwright e2e
Status
Chrome Web Store · v1.0.0
Store
Chrome Web Store

Contribution

What I built

  1. 01Designed five block modes per site — password overlay, redirect, close tab, fake loader, and a browser-style error page picked from nine Chrome and Cloudflare templates.
  2. 02Kept it zero-server: the master password exists only as a bcrypt hash, rules and that hash ride the profile's sync storage, and logs, sessions and settings never leave the device.
  3. 03Chose two interception points: redirects are stopped before the page loads, every other mode at navigation commit, with the overlay mounted at document_start inside a closed shadow root the page cannot touch.
  4. 04Added the safety valves: timed unlocks (5 / 30 / 60 minutes or today), an optional WebAuthn gate on changing the master password, and scheduled backups that work inside MV3's alarm + downloads constraints and exclude the hash.
  5. 05Tested it like a product: 44 Vitest unit specs plus 28 Playwright end-to-end specs that launch a persistent Chrome with the unpacked build loaded.

Screens

A password lock card covering example.com, asking for the master password with a one-hour unlock selector
The lock overlay rendered inside a closed shadow root over a protected page, with the unlock window chosen at the moment of entry.
Wikipedia dimmed behind a spinner card reading "Loading en.wikipedia.org…"
Fake loader mode: the page is reachable but never arrives, which turns out to deter a reflex visit better than a hard block.
Security tab of the options page with current, new and confirm password fields and a success message
Setting the master password. Only the bcrypt hash is ever stored, and the change can be gated behind a platform authenticator.
Site Lock toolbar popup showing example.com locked with Password Lock and a "Lock this site" action
The toolbar popup: current rules at a glance and a one-click lock for whatever tab is open.

How it works

How the data moves

The core paths through the system — a dot travels each hop, and the caption says what happens there.

Opening a locked site

The service worker decides at commit; the overlay asks for the password inside a closed shadow root.

NOYESYESNONOYESStart
Navigation commits (onCommitted)
A rule matches this domain?
Active unlock session?
Overlay mounts in a closed shadow root
Page loads normally
Still unlocked — page loads
bcrypt.compare(entered, hash)
Correct?
Session for 5 / 30 / 60 min or today · unlock logged
End

Swipe sideways to see the whole chart.

01 / 13GET site

You open a site in the top frame.

From the code

A few lines that matter

The password never leaves the devicesrc/shared/crypto.ts
import * as bcrypt from 'bcryptjs'

const ROUNDS = 10

export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, ROUNDS)
}

export async function verifyPassword(password: string, hash: string): Promise<boolean> {
  return bcrypt.compare(password, hash)
}

Only the bcrypt hash is stored; the overlay compares against it locally.

Intercepting at navigation commitsrc/background/index.ts
chrome.webNavigation.onCommitted.addListener(async ({ tabId, url, frameId }) => {
  if (frameId !== 0) return
  if (!url.startsWith('http://') && !url.startsWith('https://')) return

  const hostname = new URL(url).hostname
  await deleteExpiredSessions()

  const rules = await getAllRules()
  // Skip redirect rules — handled by onBeforeNavigate
  const rule = rules.find(r => r.enabled && r.mode !== 'redirect' && matchesDomain(hostname, r.domain))
  if (!rule) return

  const session = await getActiveSession(rule.id)
  if (session) return

  await addLog({ ruleId: rule.id, domain: hostname, mode: rule.mode, event: 'blocked' })
  // …then lock the tab according to rule.mode

Top frame only, expired sessions swept first, active unlocks respected, every block logged locally.

Decisions

Calls that shaped it

On-device only, no backend at all

A site blocker sees every URL you visit; the only trustworthy version of that is one with nothing to send it to.

bcrypt inside the extension, verified in the overlay

The hash is compared where the prompt lives, so the password is never stored or shipped anywhere.

Two interception points, chosen per block mode

Redirects must happen before the page loads; overlays need a document — so each mode hooks the earliest point that can do its job.

Playwright with a persistent context

Extensions only load into a persistent profile, so the e2e suite drives real Chrome with the unpacked build — the same thing users install.

Stack

  • TypeScript
  • React 19
  • Vite
  • CRXJS
  • Chrome Manifest V3
  • bcryptjs
  • Tailwind CSS
  • Playwright
  • Vitest
  • Next.js