Extract Zillow for-sale listings matching a complex multi-dimensional filter (price, beds/baths, sqft, lot, year built, property type, listing status, days-on-market, HOA, monthly payment, home features). Constructs the filtered SRP URL via Zillow's searchQueryState parameter, fetches via Browserbase Fetch API (bypasses PerimeterX), and parses __NEXT_DATA__ for structured listings + region totals + pagination. Read-only.
Works with
AI-first code editor with Composer
Before installing skills in Cursor, ensure your development environment meets these requirements:
node --versionextract-listingsExecute the skills CLI command in your project's root directory to begin installation:
Fetches extract-listings from zillow.com/extract-listings-zw43tv and configures it for Cursor.
The CLI shows a list of agents. Use arrow keys and space to select Cursor:
Confirm successful installation by checking the skill directory location:
Restart Cursor to activate extract-listings. Access via /extract-listings in your agent's command palette.
We perform automated surface-level scans (Gen AI Scanner, Socket, Snyk) during installation. These checks detect common vulnerabilities but do not guarantee complete security. Always review skill source code and verify the publisher's reputation before production use.
Skills execute code in your environment. Always review source, verify the publisher, and test in isolation before production.
Submit your Claude Code skill and start earning
Automate repetitive workflows and reduce manual effort
Example
Generate reports, summarize documents, draft communications
Save 3-5 hours per week on routine tasks
Learn new skills, understand complex topics, get expert guidance
Example
Explain concepts, provide examples, suggest learning resources
Accelerate learning and skill development by 2x
Enhance output quality through reviews, suggestions, and refinements
Example
Review drafts, suggest improvements, catch errors
Improve work quality by 30-40% with less effort
0
total installs
0
this week
0
upvotes
Run in your terminal
0
installs
0
this week
—
stars
| name | extract-listings |
| title | Zillow Filtered Listings Extraction |
| description | >- Extract Zillow for-sale listings matching a complex multi-dimensional filter (price, beds/baths, sqft, lot, year built, property type, listing status, days-on-market, HOA, monthly payment, home features). Constructs the filtered SRP URL via Zillow's searchQueryState parameter, fetches via Browserbase Fetch API (bypasses PerimeterX), and parses __NEXT_DATA__ for structured listings + region totals + pagination. Read-only. |
| website | zillow.com |
| category | real-estate |
| tags | - real-estate - listings - zillow - search - perimeterx |
| source | 'browserbase: agent-runtime 2026-05-19' |
| updated | '2026-05-19' |
| recommended_method | api |
| alternative_methods | - method: api rationale: >- Browserbase Fetch API (browse cloud fetch --proxies) returns 200 OK with full __NEXT_DATA__ on filtered Zillow SRP URLs. This is the recommended path — single HTTP call per page, no anti-bot challenge. - method: browser rationale: >- Zillow's PerimeterX 'Press & Hold' bot defense aggressively blocks Browserbase Chromium fleet across all stealth/proxy/region/captcha-solver combos verified during iteration. Use only as fallback if the Fetch API path begins returning 4xx — and even then expect 5–10× the turn count of the API path. |
| verified | true |
| proxies | true |
Given a location (city + state, ZIP, neighborhood, full Zillow URL, or free-form
region) and a multi-dimensional filter spec (price, beds, baths, sqft, lot size,
year built, days-on-market, property type, listing status, HOA, monthly payment,
home features), return the matching active for-sale listings as structured JSON
— including zpid, formatted + raw price, beds, baths, interior sqft, lot size
with unit, full address, property type, listing status, days on Zillow,
Zestimate (when present), HOA (when present), monthly-payment estimate (when
present), primary photo URL, and the canonical detail URL — plus region-wide
totalResultCount, totalPages, and the exact searchQueryState URL used.
Read-only — never click Save, Tour, Contact Agent, or any mutation control.
__NEXT_DATA__
blob is faster, structurally richer, and (via browse cloud fetch --proxies)
bypasses Zillow's anti-bot wall that hard-blocks scripted browsers.Zillow's SRP is a Next.js app that server-renders the full search state into
a <script id="__NEXT_DATA__"> JSON blob on the initial HTML response. The
canonical filter representation is the searchQueryState URL query parameter
(URL-encoded JSON). Constructing a filtered URL upfront and reading the
response's __NEXT_DATA__ is the right path — fetching unfiltered results and
post-filtering client-side is wasteful and lossy.
The optimal transport is browse cloud fetch --proxies (Browserbase's
lightweight HTTP API path), NOT a scripted browser session. Zillow fronts
the site with PerimeterX / HUMAN's "Press & Hold" bot defense, which fires a
hard JS-challenge modal on the very first navigation from a Browserbase-
fingerprinted Chromium — even bare https://www.zillow.com/austin-tx/ gets
blocked across regions (us-east-1, us-west-2, eu-central-1) and across
--verified / --proxies / --solve-captchas flag combinations. The Fetch
API path uses a different HTTP stack + residential-proxy pool that PerimeterX
serves freely (verified 200 OK on identical URLs that the browser path got 403
on, same minute).
Map free-form input to one of Zillow's path-based region slugs. The slug locks
the region; filters layer on top via searchQueryState. Use the slug WITHOUT
filters first if the region resolution looks ambiguous — it's idempotent.
| Input shape | Slug pattern | Example |
|---|---|---|
| City + state | /{city-lowercased-hyphenated}-{state-2letter-lc}/ | Austin, TX → /austin-tx/ |
| ZIP code | /homes/{zip}/ | 30307 → /homes/30307/ |
| Neighborhood | /{nbhd-slug}-{city-slug}-{state}/ | Mission, San Francisco → /mission-san-francisco-ca/ |
| Full Zillow URL | Use as-is | https://www.zillow.com/austin-tx/houses/ |
| Free-form region | Fall back to homepage search resolver (see Gotchas) | "South Bay Area" → needs lookup |
After fetching, verify searchPageState.queryState.regionSelection in the
response — it returns [{regionId: N, regionType: T}] where:
regionType: 4 — stateregionType: 6 — cityregionType: 7 — ZIPregionType: 8 — neighborhoodIf the region didn't resolve as expected (e.g., 404 or wrong city), fall back to fetching the homepage and using the search-as-you-type endpoint (see Gotchas).
searchQueryState with the full filter surfacesearchQueryState is a URL-encoded JSON object. Minimum viable shape:
{
"pagination": {"currentPage": 1},
"isMapVisible": false,
"isListVisible": true,
"regionSelection": [{"regionId": 10221, "regionType": 6}],
"filterState": { /* see below */ }
}
mapBounds is optional — Zillow fills it server-side from the region. Omit
unless you specifically need to scope to a sub-bbox of the region.
filterState schema — these keys are the long-form id values (NOT the
URL-bar shortIds like con/sf). All 110 keys come from
searchPageState.filterDefinitions in any SRP response — print that map once
per Zillow build to verify.
true for ALL → everything included)To narrow to a subset, explicitly set the unwanted types to false. Setting
the wanted type to true alone does nothing because they're all-on by default.
| Filter id | shortId | Label |
|---|---|---|
isSingleFamily | sf | Houses |
isCondo | con | Condos/Co-ops |
isTownhouse | tow | Townhomes |
isMultiFamily | mf | Multi-family |
isApartment | apa | Apartments |
isApartmentOrCondo | apco | Apt-or-condo composite — set false when narrowing away from condos |
isManufactured | manu | Manufactured |
isLotLand | land | Lots/Land |
"filterState": {
"isCondo": {"value": false},
"isMultiFamily": {"value": false},
"isApartment": {"value": false},
"isManufactured": {"value": false},
"isLotLand": {"value": false},
"isApartmentOrCondo": {"value": false}
// isSingleFamily + isTownhouse stay default true → houses + townhouses only
}
| Filter id | shortId | Default | Label |
|---|---|---|---|
isForSaleByAgent | fsba | true | Agent listed |
isForSaleByOwner | fsbo | true | Owner posted |
isNewConstruction | nc | true | New construction |
isComingSoon | cmsn | true | Coming soon |
isAuction | auc | true | Auctions |
isForSaleForeclosure | fore | true | Foreclosures |
isPreMarketForeclosure | pmf | false | Foreclosed (pre-market) |
isPreMarketPreForeclosure | pf | false | Pre-foreclosures |
isRecentlySold | rs | false | Recently sold |
isPendingListingsSelected | pnd | false | Pending & under contract |
isAcceptingBackupOffersSelected | abo | false | Accepting backup offers |
isOpenHousesOnly | open | false | Open-house only |
"For sale" is the default — you only need to flip statuses when filtering to
something non-default. Example: sold listings only → set isForSaleByAgent: false,
isForSaleByOwner: false, isNewConstruction: false, isAuction: false,
isForSaleForeclosure: false, isComingSoon: false, isRecentlySold: true.
{min, max} — both nullable)| Filter id | shortId | Unit |
|---|---|---|
price | (none — uses full id) | USD |
monthlyPayment | mp | USD/month — mortgage + tax + HOA estimate |
beds | (none) | integer |
baths | (none) | number (half-baths allowed → 1.5, 2.5) |
sqft | (none) | interior living area |
lotSize | lot | sqft by default; {min, max, units: "acre"} for acres |
built | (none) | year |
hoa | (none) | max USD/month (set max, leave min: null) |
parkingSpots | parks | {min} only |
"price": {"min": 400000, "max": 750000},
"beds": {"min": 3},
"baths": {"min": 2},
"sqft": {"min": 1500},
"built": {"min": 1990},
"hoa": {"max": 200},
"lotSize":{"min": 5000, "max": 20000}
| Filter id | shortId | Type | Values |
|---|---|---|---|
doz | (none) | Enum | "1", "7", "14", "30", "90", "6m", "12m", "24m", "36m", "any" |
sortSelection | sort | Enum | "globalrelevanceex" (Homes for You — default on SRP), "days" (newest), "priced" (low→high), "pricea" (high→low), "beds", "baths", "size", "lot", "built", "saved", "listingstatus", "featured", "paymentaa" |
keywords | att | String | free text — searches listing descriptions |
ageRestricted55Plus | 55plus | Enum | "i" (include), "e" (exclude), "o" (only) |
dataSourceSelection | dsrc | String | "all" (default) |
false → set true to require)| Filter id | shortId | Label |
|---|---|---|
hasGarage | gar | Must have garage |
parkingSpots (Range) | parks | Parking spots {min} |
hasAirConditioning | ac | Must have A/C |
hasPool | pool | Must have pool |
isWaterfront | wat | Waterfront |
singleStory | sto | Single-story only |
hasBasement | hbas | Has basement |
isBasementFinished | basf | Finished basement |
isBasementUnfinished | basu | Unfinished basement |
hasDisabledAccess | disac | Accessible |
isCityView / isMountainView / isParkView / isWaterView | cityv / mouv / parkv / watv | View attributes |
is3dHome | 3d | Has 3D tour |
onlyWithPhotos | (none) | Has photos |
onlyPriceReduction | (none) | Price-reduced |
Setting hoa.max filters to listings whose HOA is at or below that value
— it does NOT include listings with unknown HOA data unless you also set:
"includeHomesWithNoHoaData": {"value": true} // default = true
If the user wants "no HOA fee at all", combine hoa: {max: 0} with
includeHomesWithNoHoaData: {value: true} and explicitly check hdpData.homeInfo
for hoaFee after extraction — see Gotchas.
const sqs = {
pagination: {currentPage: 1},
isMapVisible: false,
isListVisible: true,
regionSelection: [{regionId: 10221, regionType: 6}],
filterState: { /* keys above */ }
};
const url = `https://www.zillow.com/austin-tx/?searchQueryState=${encodeURIComponent(JSON.stringify(sqs))}`;
For page N > 1, BOTH set searchQueryState.pagination.currentPage = N AND
insert /N_p/ into the path:
https://www.zillow.com/austin-tx/2_p/?searchQueryState=... # page 2
https://www.zillow.com/austin-tx/3_p/?searchQueryState=... # page 3
The pagination.nextUrl / pagination.previousUrl fields in the response
(searchPageState.cat1.searchList.pagination) tell you the next path slug
verbatim — use those when paginating.
export BROWSERBASE_API_KEY="$BB_API_KEY"
browse cloud fetch "$URL" --proxies --allow-redirects --output /tmp/srp.html
Verified 200 OK, ~960KB HTML, full __NEXT_DATA__ blob on the filtered URL.
The browser path returns the PerimeterX "Press & Hold" challenge instead
(see Gotchas — this is the dominant gotcha on Zillow).
__NEXT_DATA__const m = html.match(/<script id="__NEXT_DATA__" type="application\/json">([\s\S]+?)<\/script>/);
const data = JSON.parse(m[1]);
const sps = data.props.pageProps.searchPageState;
const listings = sps.cat1.searchResults.listResults; // 41 listings/page
const total = sps.cat1.searchList.totalResultCount; // region-wide total post-filter
const pages = sps.cat1.searchList.totalPages; // total pages
const perPage = sps.cat1.searchList.resultsPerPage; // 41 in observed responses
const nextSlug = sps.cat1.searchList.pagination?.nextUrl; // e.g. "/austin-tx/2_p/"
const filterEcho = sps.queryState.filterState; // ← verify your filter was accepted
const regionEcho = sps.queryState.regionSelection; // ← verify region resolution
Each entry in listResults[] has:
| Path | Description |
|---|---|
zpid (string) — also hdpData.homeInfo.zpid (number) | Canonical property id |
price (string, e.g. "$480,000") + unformattedPrice (number, e.g. 480000) | Display + raw price |
beds, baths (numbers) | Bedrooms/bathrooms — baths is a number that may be 1.5, 2.5 (full + half summed) |
area (number, sqft) | Interior living area (also hdpData.homeInfo.livingArea) |
hdpData.homeInfo.lotAreaValue + hdpData.homeInfo.lotAreaUnit | Lot size + unit ("sqft" or "acres") |
address (string) | "123 Main St, Austin, TX 78704" |
addressStreet, addressCity, addressState, addressZipcode | Parts |
latLong ({latitude, longitude}) | Lat/Lon |
statusType ("FOR_SALE", "FOR_RENT", "RECENTLY_SOLD", ...) | Listing status enum |
rawHomeStatusCd ("ForSale", "Pending", ...) | Raw status code |
marketingStatusSimplifiedCd ("For Sale by Agent", "For Sale by Owner", "New Construction", "Coming Soon", "Foreclosure", "Auction") | Marketing variant |
statusText ("Active", "Pending", ...) | Display label |
hdpData.homeInfo.homeType ("SINGLE_FAMILY", "CONDO", "TOWNHOUSE", "MULTI_FAMILY", "APARTMENT", "MANUFACTURED", "LOT") | Property-type enum |
hdpData.homeInfo.daysOnZillow (number) | Days on market |
hdpData.homeInfo.taxAssessedValue (number, optional) | Tax-assessed value |
zestimate (number, top-level, optional) | Zillow estimate when shown |
imgSrc (string) | Primary photo URL |
detailUrl (string — already absolute or use https://www.zillow.com prefix) | Canonical detail page |
carouselPhotosComposable (array, optional) | Additional photo URLs |
hdpData.homeInfo.listing_sub_type (object) | {is_FSBA: true}, {is_FSBO: true}, {is_newHome: true}, {is_foreclosure: true}, etc. |
Per-listing HOA (hoaFee) and monthly-payment-estimate fields are NOT
consistently present in listResults[]. Zillow stores those on the property's
detail page (/homedetails/.../<zpid>_zpid/) — if the user requires them,
follow the detailUrl and parse its __NEXT_DATA__ (gdpClientCache →
property keys). See Gotchas.
If totalPages > 1 AND the caller wants all results, fetch each /N_p/?searchQueryState=...
page (incrementing pagination.currentPage AND the /N_p/ path segment in
tandem). Zillow caps SRP results at the first ~500 listings (≤ 13 pages of
41) regardless of totalResultCount. If totalResultCount > 500, set
resultsCapped: true in the output and advise the caller to narrow filters.
Emit the JSON schema in "Expected Output" below. Include the exact
searchQueryState_url you used and the echoed filterState_applied from
the response (Zillow occasionally normalizes / drops unrecognized keys — the
echo is the source of truth).
PerimeterX "Press & Hold" hard-blocks the browser path. A scripted
Chromium (Browserbase browse open --remote) on https://www.zillow.com/austin-tx/
returns a page whose <title> is "Access to this page has been denied" and
body reads "Press & Hold to confirm you are a human (and not a bot)" with a
PerimeterX reference ID. Verified across:
--verified alone--verified --proxies--verified --proxies --solve-captchasus-west-2, us-east-1, eu-central-1/houses/), and
full searchQueryState URLsThe very first session occasionally renders the bare SRP successfully but
shows the Press & Hold modal over the page; any subsequent navigation
hard-locks at the HTTP level. PerimeterX appears to fingerprint Browserbase's
Chromium fleet — --solve-captchas does NOT solve the Press & Hold puzzle
(it's a long-hold gesture, not a checkbox/click captcha). Don't fight it
— use the Fetch API.
browse cloud fetch --proxies bypasses PerimeterX entirely. Verified
200 OK on identical URLs that the browser path 403'd on, in the same minute.
The Fetch API path uses a different HTTP stack + residential-proxy pool that
Zillow's PerimeterX rules don't fingerprint. Always pass --proxies —
bare Fetch without proxies may also be flagged on follow-up requests.
Always pass --allow-redirects since Zillow 301's bare-IP hits to
geo-localized variants.
__NEXT_DATA__ is the full SRP state. No XHR scraping needed —
props.pageProps.searchPageState contains:
queryState (echo of the applied searchQueryState)filterDefinitions (the full 110-filter schema — IDs, types, defaults,
shortIds, allowed enums)cat1.searchResults.listResults (the page's listings)cat1.searchList.{totalResultCount, totalPages, resultsPerPage, pagination}filterState keys are long-form IDs, not URL-bar shortIds. The URL bar
may show e.g. ?searchQueryState=...sortSelection... but ALSO Zillow's SEO
paths use shortIds like /3-_beds/. In the JSON body, use the long form
(isSingleFamily, not sf; hasGarage, not gar). The shortIds are only
for the path-segment SEO URLs.
Property-type filters are all-on by default. Setting isSingleFamily: {value: true} alone does NOTHING — every property type is included by
default. To narrow to one type, set the other types to {value: false}
(and remember isApartmentOrCondo is a composite that must also be false
when narrowing away from condos).
Pagination requires BOTH the path AND the query state. Page 2 of Austin
is /austin-tx/2_p/?searchQueryState=...pagination.currentPage=2.... Setting
only one of them silently returns page 1. Use the response's
searchList.pagination.nextUrl as the authoritative path slug.
41 results per page, capped at ~500 total. resultsPerPage is 41 in
every observed response. Zillow's SRP scroll caps at ~500 results
(~13 pages) — beyond that, the SRP returns the same last page and totalPages
reflects the cap, not the true total. Mirror totalResultCount > 500 →
resultsCapped: true in your output and advise the caller to narrow filters
(tighter geography, tighter price band, etc.).
Lot-size units toggle. lotSize defaults to sqft ({min, max, units: "sqft"}). To filter by acres, set units: "acre" AND interpret
hdpData.homeInfo.lotAreaUnit in the response — Zillow may return some
listings in acres and some in sqft (typically sqft below 1 acre, acres above).
HOA filter behavior. hoa: {max: N} keeps listings with HOA ≤ N. To
also include unknown-HOA listings, ensure includeHomesWithNoHoaData: {value: true} (the default). For "zero HOA only": hoa: {max: 0} AND check
hdpData.homeInfo.hoaFee post-extraction.
Per-listing hoaFee, zestimate, monthlyPayment are inconsistently
present in listResults[]. zestimate appears on some listings (~30% in
Austin sample), hoaFee and monthlyPayment are usually absent at the SRP
level. To fetch them reliably, follow detailUrl and parse the detail
page's __NEXT_DATA__ (gdpClientCache → ForSaleShopperPlatformFullRenderQuery
→ property → hoaFee, zestimate, monthlyHoaFee, monthlyHoaFeeDisplay).
Only do this when the caller specifically asks — it's N extra fetches.
Region slug for free-form input falls back to homepage search. ZIP
(/homes/{zip}/), city + state (/{slug}-{state}/), and most major
neighborhoods (/{nbhd}-{city}-{state}/) work via direct path. For unknown
free-form regions ("South Bay Area", "DFW Metroplex"), fetch the homepage
https://www.zillow.com/homes/ and submit the term to the search resolver:
Zillow's autocomplete GraphQL endpoint (/zg-graph/autocomplete/results)
returns 400 without Apollo client headers (x-apollo-operation-name,
client-id, x-caller-id) — easiest path is to slugify the input
heuristically and fall back to a 404-detection retry chain.
mapBounds is optional. Zillow fills it server-side from regionSelection.
Including it scopes to a sub-bbox; omitting it gives the full region.
Sold listings need isRecentlySold: true AND all for-sale toggles set to
false. Otherwise the SRP returns the union of for-sale + recently-sold.
doz Enum, not Range. Days-on-Zillow is {value: "30"} (string), not
{min: 0, max: 30}. Only the listed enum values ("1", "7", "14",
"30", "90", "6m", "12m", "24m", "36m", "any") are accepted —
unrecognized values default to "any".
searchPageState.filterDefinitions is the canonical schema source.
When in doubt about a filter's shape (Boolean? Range? Enum? what's the
default?), fetch ANY SRP response and read its filterDefinitions block.
Zillow updates the schema over time — re-derive once per quarter.
Detail URLs are canonical. detailUrl is https://www.zillow.com/homedetails/{slug}/{zpid}_zpid/
— the _zpid/ suffix is mandatory and the slug is informational. Hitting
just https://www.zillow.com/homedetails/{zpid}_zpid/ also redirects to
the canonical.
READ-ONLY. Do NOT click Save, Save Search, Tour, Contact Agent, Apply, or any other CTA on the SRP or detail pages. The skill never makes a state change.
If browse cloud fetch --proxies starts returning 4xx (e.g., Zillow extends
PX to the Fetch path):
browse cloud sessions create --keep-alive --verified --proxies --solve-captchas --region us-east-1 session.https://www.zillow.com/ (homepage, NOT the regional SRP first).wait timeout 6000 for any PX challenge animation to finish.browse fill to enter the location in the search box, browse press Enter. This routes through the SPA and may bypass the direct-URL
PX fingerprint.__NEXT_DATA__ via browse eval:
JSON.parse(document.getElementById('__NEXT_DATA__').textContent).props.pageProps.searchPageStatebrowse fill / browse click, then browse click the "Apply"
button. Zillow's SPA will update searchQueryState via history.pushState
without triggering a hard navigation — this avoids the PX fingerprint.wait timeout 2000 and re-read __NEXT_DATA__ /
window.__INITIAL_STATE__ for fresh results.This path costs ~5–10× more turns than the Fetch path and is brittle to PX modal interrupts. Use Fetch API as primary; fall back to browser only when Fetch breaks.
{
"success": true,
"searchQueryState_url": "https://www.zillow.com/austin-tx/?searchQueryState=%7B%22pagination%22%3A%7B%22currentPage%22%3A1%7D%2C...",
"filterState_applied": {
"sortSelection": {"value": "globalrelevanceex"},
"price": {"min": 400000, "max": 750000},
"beds": {"min": 3, "max": null},
"baths": {"min": 2, "max": null},
"sqft": {"min": 1500, "max": null},
"built": {"min": 1990, "max": null},
"doz": {"value": "30"},
"isCondo
Prerequisites
Time Estimate
15-45 minutes depending on use case complexity
Steps
Common Pitfalls
✓ Do
✗ Don't
💡 Pro Tips
✓ Use when
Use when skill capabilities match your task, clear ROI on time saved, and you can validate outputs. Best for repetitive tasks, learning, and quality improvement.
✗ Avoid when
Avoid when task requires deep expertise you can't validate, involves sensitive decisions, or when learning process is more valuable than speed of completion.
depop.com/search-listings-p8xawn
kostja94/marketing-skills
aaaaqwq/claude-code-skills
agentbay-ai/agentbay-skills
glebis/claude-skills
shopmeskills/mcp
extract-listings fits our agent workflows well — practical, well scoped, and easy to wire into existing repos.
extract-listings fits our agent workflows well — practical, well scoped, and easy to wire into existing repos.
extract-listings is among the better-maintained entries we tried; worth keeping pinned for repeat workflows.
extract-listings is among the better-maintained entries we tried; worth keeping pinned for repeat workflows.
Keeps context tight: extract-listings is the kind of skill you can hand to a new teammate without a long onboarding doc.
Keeps context tight: extract-listings is the kind of skill you can hand to a new teammate without a long onboarding doc.
Useful defaults in extract-listings — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
I recommend extract-listings for anyone iterating fast on agent tooling; clear intent and a small, reviewable surface area.
I recommend extract-listings for anyone iterating fast on agent tooling; clear intent and a small, reviewable surface area.
Useful defaults in extract-listings — fewer surprises than typical one-off scripts, and it plays nicely with `npx skills` flows.
showing 1-10 of 32