All skills
harlan-zw avatar

/unlighthouse

@c3d5c5f
by Harlan Wiltonharlan-zw/unlighthouse4.9k stars
178
Run it yourself

Run Google Lighthouse on every page of a site with the unlighthouse CLI, and fail CI on low scores with unlighthouse-ci. Use when a task mentions unlighthouse, unlighthouse-ci, site-wide Lighthouse scans, performance budgets in CI, unlighthouse.config.ts, defineUnlighthouseConfig, scanner.exclude, dynamic sampling, the authenticate hook, or a scan that never exits, skips pages, or refuses to clear its output folder.

Use this Skill: https://skilld.dev/gh/harlan-zw/unlighthouse/unlighthouse

Nothing lands on disk. Nothing to clean up.

Fork this Skill

Edit a local copy. It keeps the author and licence.

SKILL.md

≈108 tokens for metadata: the name and description. ≈1.9k when used: this file.

Description uses 5.4% of example budget

Before choosing a Skill, your Agent reads its name and description. All available Skills share that space.

  • A shorter description leaves more room for other Skills. This entry exceeds our 1% size suggestion.

In our Claude Code example, all Skill names and descriptions share 8,000 characters. This Skill uses ≈434 characters, or 5.4%.

The 1% threshold is a size suggestion. Longer descriptions can still fit.

Your model, settings, and other Skills decide how much text your Agent can read.

Example settings and source

The example uses a 200k-token context and default Claude Code settings. The count includes the name, description, separators, and when_to_use when present. Codex also counts local file paths.

Skit's source and limits: Codex 0.160.1, Claude Code 2.1.292.

unlighthouse

Tested on Node 24 against the unlighthouse release that ships this Skill (requires Node >=22.18.0). The package finds the URLs of a site and runs Lighthouse on each one in parallel Chrome instances. It ships two binaries: unlighthouse opens a live dashboard, and unlighthouse-ci exits with a status code. Docs: https://unlighthouse.dev

Setup

npx unlighthouse-ci --site https://example.com --budget 80
  • Install unlighthouse for both binaries. @unlighthouse/cli ships only unlighthouse-ci.
  • Do not install puppeteer. puppeteer-cluster already depends on it.
  • Chrome: the package uses the system Chrome. If there is none, it downloads Chrome to ~/.unlighthouse. As root, it adds --no-sandbox itself.
  • Add .unlighthouse to .gitignore.
  • @unlighthouse/nuxt, @unlighthouse/vite, and @unlighthouse/webpack are deprecated. Run the CLI against the dev server URL instead.
  • The config file is unlighthouse.config.ts in the current directory. A CLI flag you pass wins over the config file. A flag you leave out keeps the config value.

Pick the binary

  • unlighthouse-ci scans, writes reports, and exits. Exit code 1 means a budget failed, or no route was left to scan. Use it in scripts, CI, and Agent runs.
  • unlighthouse starts a dashboard server (port 5678 by default), opens a browser, and keeps running after the scan. It never exits by itself.

Automatic behaviour

  • URL discovery: urls if set, else robots.txt, then sitemap.xml, then a crawler that follows links from the home page. Robots.txt Disallow lines become excludes.
  • --urls or urls turns off robots.txt, sitemap, crawler, and sampling. The paths are relative to site.
  • A site with a path, such as https://example.com/docs, turns off sitemap, robots.txt, and sampling.
  • A sitemap with 50 or more URLs turns off the crawler, except on localhost.
  • Dynamic sampling scans at most 8 random URLs per route group, such as /blog/*. Turn it off with --disable-dynamic-sampling.
  • scanner.maxRoutes (200) stops the queue. It logs a warning and exits 0.
  • HTML inspection skips JavaScript. If the home page has no links, the crawler turns JavaScript on and retries. For an SPA, pass --enable-javascript.
  • Device is mobile. Pass --desktop for desktop.
  • Throttling is simulated for a remote site and off for a local one (localhost, 127.0.0.1). An explicit scanner.throttle or --throttle wins.
  • Redirects: if site redirects to another host, such as http to https or apex to www, the scan uses the redirect target.

Common tasks

Per category budgets, reporters, and scope in one config:

import { defineUnlighthouseConfig } from 'unlighthouse/config'

export default defineUnlighthouseConfig({
  site: 'https://staging.example.com',
  scanner: {
    exclude: ['/admin/**', '/api/**'],
    include: ['/', /^\/blog\//, '/docs/**'], // keep '/' so the crawler can start
  },
  ci: {
    budget: { 'performance': 70, 'accessibility': 95, 'best-practices': 90, 'seo': 90 },
    reporter: 'jsonExpanded',
  },
})
  • Budgets use 0 to 100. Report scores use 0 to 1. A budget of 80 fails a score of 0.79.
  • Reporters: json (default), jsonExpanded, csv, csvExpanded, lighthouseServer. The file is <outputPath>/ci-result.json or ci-result.csv.
  • lighthouseServer needs --lhci-host and --lhci-build-token.

Static HTML report: pass --build-static, or set ci.buildStatic: true. It writes index.html to the root of outputPath (.unlighthouse). Upload that folder. It deletes the per page lighthouse.json files.

Authentication: the authenticate hook gets the Puppeteer Page as its first argument. It runs once. Its cookies reach every scanned page, including the Lighthouse run, and only the hosts they belong to.

import { defineUnlighthouseConfig } from 'unlighthouse/config'

export default defineUnlighthouseConfig({
  auth: { username: 'admin', password: process.env.AUTH_PASS! },
  hooks: {
    async authenticate(page) {
      await page.goto('https://example.com/login')
      await page.type('input[name="email"]', 'test@example.com')
      await Promise.all([page.waitForNavigation(), page.click('button[type="submit"]')])
    },
  },
})

Read scores in a hook: task-complete fires once per task, so check taskName. categories is an array of { key, id, title, score }.

import { defineUnlighthouseConfig } from 'unlighthouse/config'

export default defineUnlighthouseConfig({
  hooks: {
    'task-complete': (path, report, taskName) => {
      if (taskName !== 'runLighthouseTask')
        return
      const performance = report.report?.categories.find(category => category.key === 'performance')
      console.log(path, report.report?.score, performance?.score)
    },
  },
})

Programmatic scan: register worker-finished before start(). Without a provider, reports go to .unlighthouse/<host>/<config hash>/.

import { createUnlighthouse } from 'unlighthouse'

const unlighthouse = await createUnlighthouse({ site: 'https://example.com', urls: ['/', '/about'] })
unlighthouse.hooks.hook('worker-finished', async () => {
  console.log(unlighthouse.worker.reports().map(r => [r.route.path, r.report?.score]))
  await unlighthouse.worker.cluster.close()
  process.exit(0)
})
await unlighthouse.start()

Traps

  • unlighthouse-ci clears outputPath before it scans, and refuses a folder it did not create. --output-path . exits 1 with Refusing to clear ... it is not an unlighthouse output folder. Point it at a new or empty folder. Unlighthouse marks its folders with a .unlighthouse-output file.
  • String patterns are route patterns, not regex. --exclude-urls "/blog/.*" excludes nothing. * matches one segment and ** any depth, so use /blog/**, or a RegExp in the config.
  • An include list that skips / stops a crawler scan. The crawler starts from /, so nothing is queued and both binaries exit 1 with No routes left to scan. Add '/' to include, or use --urls.
  • A hook that throws stops unlighthouse-ci with exit code 1. Guard on taskName and optional fields.
  • Dynamic sampling is random. Two runs can scan different pages in one group. For stable CI results, pass --urls or turn sampling off.

Output

  • unlighthouse-ci: .unlighthouse/ci-result.json and .unlighthouse/reports/<path>/lighthouse.json.
  • unlighthouse: .unlighthouse/<host>/<config hash>/. A config change starts a new cache folder.

Config

  • scanner.device ('mobile'), scanner.samples (1, runs per page, averaged), scanner.throttle (on for remote sites, off for local ones).
  • scanner.dynamicSampling (8), scanner.maxRoutes (200), scanner.crawler, scanner.sitemap, scanner.robotsTxt.
  • puppeteerClusterOptions.maxConcurrency (half the CPU cores). Set 1 for stable performance scores.
  • lighthouseOptions passes through to Lighthouse. onlyAudits replaces onlyCategories.
  • chrome.useSystem, chrome.downloadFallbackCacheDir.
  • All options: https://unlighthouse.dev/api-doc/config

Debug

  • --debug logs the resolved config. A route that include or exclude drops logs Skipping route based on include / exclude rules.
  • "Failed to queue routes for scanning" means discovery found nothing. Check the site status and the robots.txt Disallow lines.
  • "No routes left to scan" means discovery found routes, and include, exclude, or robots.txt rules skipped all of them.

Source: SKILL.md on GitHub

skilld matched fixed text patterns in SKILL.md and file names. Patterns miss obfuscated code.

skilld run checks every file with the same patterns. It asks for approval before it loads a Skill with a behavior marked Needs approval.

1 warning10d2 checks · Risk SAFE
  • Gen Agent Trust Hub10d

    This skill provides instructions for using unlighthouse, a tool for site-wide Lighthouse audits. It includes configuration examples, authentication handling via environment variables, and automated crawling behavior. The identified behaviors, such as processing external web content and dynamic configuration, are standard for this type of developer tool and do not pose a security risk in this context.

  • Snyk10d

    Risk: MEDIUM · 1 issue

Signed by skilld at c3d5c5f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 2 days ago
compatibility
Requires Node.js >=22.18.0.

README badge

README badge for harlan-zw/unlighthouse