Back to blog
·4 min read

Add a Stealth Score Check to Your CI Pipeline

You pin your Node.js version. You lock your dependencies. You run tests on every PR. But do you test whether your automated browser is still undetectable?

Browser updates, dependency bumps, and stealth plugin changes can silently break your fingerprint. A Chromium update might expose a new automation artifact. A Playwright upgrade might change how bindings are injected. By the time you notice an increased block rate, you've already lost data.

Here's how to add a stealth score check to your CI pipeline.

How it works

  1. Your CI pipeline launches a browser (the same way your production code does)
  2. The browser visits a StealthCheck monitoring URL
  3. StealthCheck analyzes the fingerprint and returns a score
  4. Your pipeline fails if the score drops below a threshold

This requires a StealthCheck monitoring profile, which provides a token for the lightweight check page. Monitoring profiles are available on paid plans.

Setup

1. Create a monitoring profile

Create an account and set up a browser profile. Set the target browser and OS to match your production setup. On the profile detail page, you'll see a monitoring URL (e.g., https://stealthcheck.io/check/a1b2c3d4-...). Copy the full URL and store it as a STEALTH_CHECK_URL secret in your CI provider.

2. Add the check to your test suite

The lightweight check page at /check/{token} runs the fingerprint analysis and exposes the result on window.__stealthcheck. The page adds a .done CSS class to the status element when analysis is complete.

Playwright

// stealth.spec.ts
import { test, expect } from '@playwright/test'

test('stealth score meets threshold', async ({ page }) => {
    await page.goto(process.env.STEALTH_CHECK_URL)

    await page.waitForSelector('.done', { timeout: 30000 })

    const result = await page.evaluate(() => (window as any).__stealthcheck)

    expect(result.score).toBeGreaterThanOrEqual(90)

    if (result.score < 95) {
        console.warn(`Stealth score: ${result.score}/100`)
        console.warn(
            'Issues:',
            result.issues.map((i: { code: string }) => i.code).join(', '),
        )
    }
})

Puppeteer

// stealth.test.mjs
import puppeteer from 'puppeteer'

const browser = await puppeteer.launch({
    headless: true,
    // Use the same launch args as your production config
})
const page = await browser.newPage()

await page.goto(process.env.STEALTH_CHECK_URL)
await page.waitForSelector('.done', { timeout: 30000 })

const result = await page.evaluate(() => window.__stealthcheck)

if (result.score < 90) {
    console.error(`Stealth score too low: ${result.score}/100`)
    console.error(
        'Issues:',
        result.issues.map((i) => i.code).join(', '),
    )
    process.exit(1)
}

console.log(`Stealth score: ${result.score}/100`)
await browser.close()

3. Configure your CI

GitHub Actions (Playwright)

name: Stealth Check
on: [push, pull_request]

jobs:
  stealth:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 22

      - run: npm ci
      - run: npx playwright install chromium

      - name: Run stealth check
        run: npx playwright test stealth.spec.ts
        env:
          STEALTH_CHECK_URL: ${{ secrets.STEALTH_CHECK_URL }}

GitHub Actions (Puppeteer)

name: Stealth Check
on: [push, pull_request]

jobs:
  stealth:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 22

      - run: npm ci

      - name: Run stealth check
        run: node stealth.test.mjs
        env:
          STEALTH_CHECK_URL: ${{ secrets.STEALTH_CHECK_URL }}

GitLab CI

stealth-check:
  image: mcr.microsoft.com/playwright:v1.52.0
  script:
    - npm ci
    - npx playwright test stealth.spec.ts
  variables:
    STEALTH_CHECK_URL: $STEALTH_CHECK_URL

Choosing a threshold

  • 95+ is strict. Minor Chrome updates may cause temporary dips.
  • 90 catches significant regressions while tolerating minor fluctuations.
  • 85 is lenient. Acceptable if your use case tolerates some detection risk.

Reading the results

When the score drops, the issues array tells you what changed:

{
    "score": 82,
    "issues": [
        {
            "code": "webdriver_true",
            "category": "automation",
            "severity": "critical",
            "title": "navigator.webdriver is true",
            "description": "...",
            "recommendation": "..."
        },
        {
            "code": "plugins_missing",
            "category": "consistency",
            "severity": "warning",
            "title": "No browser plugins detected",
            "description": "...",
            "recommendation": "..."
        }
    ]
}

Each issue includes a category, severity (critical, warning, info), and a recommendation for how to fix it.

Common CI regressions

Cause Example Fix
Chrome update New version changes WebGL behavior or adds APIs Pin Chrome version or update stealth patches
Playwright/Puppeteer update Changed launch flags or binding injection Pin version, test before upgrading
CI environment change New runner image with different fonts or locale Set explicit font and locale configuration
Dependency update Stealth plugin changed or removed a patch Pin plugin version, check changelog

Further reading

Test your browser stealth for free

Run a free fingerprint test and see exactly what detection systems see.