create-scraper — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited create-scraper (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Your goal is to explore a company's careers page and write a permanent, reliable Scraper subclass that other code can use going forward. The argument is a careers page URL, e.g. /create-scraper https://explore.jobs.netflix.net/careers.
Derive the class name and filename from the company's domain or brand name. E.g.:
explore.jobs.netflix.net → NetflixScraper → src/scraping/NetflixScraper.tsjobs.stripe.com → StripeScraper → src/scraping/StripeScraper.tsThe goal is to find a JSON API the page calls internally — these are far more reliable than DOM scraping.
browser_navigatebrowser_evaluate:window.__reqs = [];
const _f = window.fetch.bind(window);
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
window.__reqs.push({ url, method: (init?.method || 'GET').toUpperCase() });
return _f(input, init);
};
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
window.__reqs.push({ url: String(url), method: method.toUpperCase() });
return _open.apply(this, arguments);
};browser_snapshot to understand the page structurewindow.__reqs/api/jobs, /search, /v1/positions, /careers/api, etc. Filter out analytics, fonts, images, and tracking pixels.If you found a promising API endpoint:
browser_evaluate to call it directly and inspect the response shape:const res = await fetch('ENDPOINT_URL');
const data = await res.json();
JSON.stringify(data, null, 2).slice(0, 3000); // previewtotal, offset, limit, page, cursor, or similar fields. Test by modifying query params.If the site uses Greenhouse or Lever, stop — those are already handled by GreenhouseScraper and LeverScraper. Report this to the user instead of writing a new scraper.
Click through to page 2 (or trigger a "Load more"), confirm the data pattern holds, and understand the full pagination mechanism you'll need to implement.
Navigate to 1–2 individual job listings:
browser_snapshot the detail pageUnderstand what data is available: full description, requirements, salary, etc. This is what getJobContent will return.
Now write src/scraping/{Name}Scraper.ts. The approach depends on what you found above.
If you found a reliable JSON API:
import { cachedFetch } from '../fetch.ts';
import { Scraper, type ListedJob } from './Scraper.ts';
export class {Name}Scraper extends Scraper {
async getJobsList(testing = false): Promise<ListedJob[]> {
// Fetch all pages via the API and return the full list
// if `testing` is true, only fetch a single job (if possible) to speed up tests
}
async getJobContent(id: string): Promise<string> {
// Call the job detail API endpoint and return JSON.stringify(data)
}
}If no usable API exists, scrape the DOM with Playwright. Ask the user to confirm before adding the dependency (yarn add playwright), then write the scraper using the selectors and patterns you identified while exploring.
Use the accessibility snapshot you took in Step 2 to identify the most stable selectors — prefer aria-label, role, and data-* attributes over class names, which tend to change. If a job row has a consistent structure in the snapshot (e.g. a link with a heading inside it), reflect that in your selectors.
import { chromium } from 'playwright';
import { Scraper, type ListedJob } from './Scraper.ts';
export class {Name}Scraper extends Scraper {
private async withPage<T>(fn: (page: import('playwright').Page) => Promise<T>): Promise<T> {
const browser = await chromium.launch({ headless: true });
try {
return await fn(await browser.newPage());
} finally {
await browser.close();
}
}
async getJobsList(testing = false): Promise<ListedJob[]> {
return this.withPage(async (page) => {
// Navigate, handle pagination, and extract jobs from the DOM
// Return ListedJob[] = { title: string; location: string; id: string }
// Use the job detail page URL (or a stable path segment) as `id`
});
}
async getJobContent(id: string): Promise<string> {
return this.withPage(async (page) => {
// Navigate to the job detail page using `id`
// Extract and return the meaningful text content of the posting
});
}
}Pagination in DOM scrapers: look for a "Next" button or "Load more" and loop until it's gone or disabled. Confirm the selector works by watching the snapshot change between pages during exploration.
id in ListedJob must be stable and usable in getJobContent to retrieve detailsgetJobContent should return a rich string — either JSON.stringify(apiResponse) or the full text of the job posting — since it's passed to the Analyzer to assess fitgetJobsList — return all jobs, not just page 1location field can be a comma-joined string if multiple locations existcompany-listings.ts accordinglyRun yarn typecheck to confirm the file compiles cleanly. Fix any type errors before reporting done.
Add the new company to scrapers.test.ts then run TEST_COMPANIES=[COMPANY_SLUG] yarn test ./src/scraping/scrapers.test.ts
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.