crawl-fetch-ingest — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited crawl-fetch-ingest (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.
Given a deck PDF and/or a firm URL, fill in the team and portfolio metadata so a designer can drop it into HTML/Tailwind/Reveal slides without hand-collecting every headshot, title, and logo.
The skill supports two starting anchors. Both are first-class. They share the same fetch cascade (Jina / Firecrawl / OpenGraph.io / Brandfetch / SVG-tier fallback / bg-strip) and the same schema. Only the entry point and stop condition differ.
Anchor: VC firm Anchor: Operating company
→ team → list of backer firms
→ portfolio cos → for each backer:
→ portco CEOs → team
→ portfolio cos
(stop — no portco CEOs)Firm-anchored (the original walk): one VC firm is the root. Used when you're rebuilding a firm's own deck, memo, or website. See ## The four checkpoints below.
Company-anchored (credibility-card walk): one operating company is the root, and we walk outward through its named backers. Used when an operating company's deck/site needs to make its investor list legible to readers who don't know those firms. The traversal stops at the backers' portfolio companies — portco CEOs add no legibility at credibility-card distance. See routines/investor-credibility-ingest.md.
Both walks can run inside the same project. A typical fundraise repo ends up with:
<project>/data/
team/ # the operating company's own employees (company-anchored, CP1-only)
investors/ # backer firms (investor-credibility-ingest routine)
{firm-a}/firm.md + team/ + portfolio/
firms/ # if the project also did the firm-anchored walk on some specific VC
{firm}/firm.md + team/ + portfolio/ + portco-ceos/The skill should accept any of:
/team, /portfolio, /about from sitemap or anchor crawl; build entity roster from the siteIf only a name is given (e.g., "fill out Sequoia Capital"), search for the homepage with Tavily, confirm with user, then proceed as URL-only.
Per-project content + assets land here:
<cwd>/data/firms/{firm-slug}/
firm.md # firm-level metadata
team/{person-slug}.md # CP1 (VC team) + CP2 (advisors) outputs
team/{person-slug}.{jpg|png} # headshot
portfolio/{co-slug}.md # CP3 output
portfolio/{co-slug}.svg # logo (svg preferred, png/jpg fallback)
portfolio/{co-slug}-ceo.md # CP4 output (CEO of that portco)
portfolio/{co-slug}-ceo.{jpg|png}Raw API responses are cached globally so the same firm's data is reused across decks/memos:
~/.claude/skills/crawl-fetch-ingest/cache/{firm-slug}/
firecrawl/{url-hash}.json
tavily/{query-hash}.json
og/{url-hash}.jsonCache lookup is the first step before any paid call. Delete the firm's cache folder to force re-fetch.
Each checkpoint is a human-confirmation gate. Run discovery, present the roster/list to the user, wait for "go" before paid fetches.
Goal: match deck names to firm-site bios; enrich with LinkedIn and other public profiles.
Important: firms scatter their people across multiple sub-pages by role, not just one /team page. A pure /team crawl misses venture partners, operating partners, EIRs, supporting partners, and the advisory board — all of whom may appear in the deck. CP1 must enumerate all of the firm's people-bearing pages, then categorize each person into the right role_class.
Page-discovery cascade. Try each path; treat 404 / missing-link as "this firm doesn't use that section." Keep going.
| Path family | Maps to role_class | Common path variants |
|---|---|---|
| Core team | vc-team, managing-partner (top of list usually) | /team, /people, /who-we-are, /about/team |
| Venture partners | venture-partner | /venture-partners, /partners, /team#venture |
| Operating partners | operating-partner | /operating-partners, /platform, /team#operating |
| Entrepreneurs in residence | entrepreneur-in-residence | /eir, /residents, /entrepreneurs-in-residence |
| Supporting partners / mentors | supporting-partner | /support, /supporting-partners, /mentors, /community, /network |
| Advisory board / LPAC | advisor | A separate section on /team, or dedicated /lpac, /advisors, /board pages |
For paths that paginate (e.g., /support?page=2), iterate until an empty page or a Load more button stops appearing.
Per-person fetch cascade. For each person discovered above:
/team/{slug}, /supporting-partners/{slug}, etc.) → markdown.{name, title, bio, headshot, profile_links}[] extraction schema.deck_role_label.og:image headshot when the firm site's <img> is small.<meta property="og:image"> — but some firms (e.g., Webflow sites) put it in background-image:url(...) on a sibling <div>. Check both.Output: team/{person-slug}.md with the appropriate role_class (see schema/person.md for the full list of 9 recognized values + disambiguation tips). All people from the firm's site go in team/, regardless of role_class — the file location stays uniform; the role_class field carries the meaning.
Note on terminology mapping. Firms use idiosyncratic labels — Calm/Storm calls their founder-mentor ecosystem "Supporting Partners," Sequoia calls it "Scouts," Bessemer has "Operating Advisors." Pick the closest matching role_class from the schema; preserve the firm's literal label in deck_role_label and title. Don't spawn new role_classes for one-off labels.
Goal: people named in the deck who didn't match anywhere in CP1's expanded discovery (any sub-page, any role). Resolve them via search.
If you've already broadened CP1 to cover all the role-bearing sub-pages above, CP2 will be smaller than it used to be — many "advisors" turn out to live on the firm's /support or /lpac page and get caught in CP1. CP2 is now genuinely "people the firm doesn't list publicly" — usually external advisors, occasional collaborators, or people the deck mentions by reputation.
Cascade:
"{Name}" "{Firm Name}" advisor or "{Name}" "{Title from deck}"status: flagged and confidence: low — never skip silentlyOutput: team/{person-slug}.md with role_class: advisor (if the deck's role label suggests a formal advisory/governance role) or external (if no clear category). Role-label hint from the deck preserved in deck_role_label.
Goal: for each company referenced in the deck, gather profile metadata + brand assets.
Principle: match the asset role to the render context. A single "the logo" doesn't exist — every company has multiple brand assets at different aspect ratios for different uses. Fetching just one and forcing it everywhere produces bad layouts (a horizontal wordmark squeezed into a square chip becomes invisible-tiny-text; a square favicon stretched to a wide header looks pixelated). The skill captures three asset roles per company by default, then the rendering layer picks whichever fits the slot:
| Role | Aspect ratio | Use case | Fetch helper |
|---|---|---|---|
trademark (or wordmark + appIcon) | wide / horizontal | inline header, wordmark display, hero ribbon | scripts/logo-hunt.sh + scripts/brandfetch.ts |
favicon | square (1:1) | small chip, tile, list-row icon, OS app icon | scripts/favicon-hunt.sh |
og:image (URL only, no download by default) | 1.91:1 social card | portfolio detail-page hero, deck banner, social share preview | scripts/og-image-hunt.sh |
The CP3 fetch loop should run all three for every portfolio company — they're cheap, they hit different paths on the company site, and they cover the most common rendering contexts a downstream deck/site will need.
Cascade:
/portfolio (or /companies, /investments) → markdown{name, website, sector, stage, description}[] schemaog:image + description"{Co} crunchbase", "{Co} linkedin") — fetch via Firecrawl (both are JS-gated)Trademark / wordmark cascade. SVG quality matters: rasters cause three recurring problems in slide / card layouts — opaque backgrounds that clash with surfaces, low-resolution that pixelates when scaled, and inconsistent margin-to-glyph ratio across brands so logos in the same-size container look visually uneven. Always try harder for an SVG before settling for a raster.
Run the cascade in this order, stopping at the first SVG (or first usable raster on the final tier). Record asset_strategy in the company's frontmatter.
<svg> elements inside <header> / <nav> / [class*="logo"]. Many modern sites inline the nav logo as SVG. If found, extract and serialize./logo.svg, /assets/logo.svg, /img/logo.svg, /static/logo.svg, /images/logo.svg, /brand/logo.svg, /favicon.svg, /apple-touch-icon.svg. Also upgrade any <img src="...png"> in the nav to .svg and try./brand, /press, /media, /kit, /brand-assets, /about/press, /company/brand, /legal/brand. These pages often link to a downloadable SVG explicitly. Jina Reader the page; grep for .svg.BRANDFETCH_API_KEY is set) — https://api.brandfetch.io/v2/brands/{domain} returns brand assets keyed by format. Prefer format=svg. Free tier: 1k req/mo. See scripts/brandfetch.ts (when added)."{Co} logo svg" site:worldvectorlogo.com OR site:seeklogo.com OR site:vectorlogo.zone OR site:wikimedia.org OR site:upload.wikimedia.org. These are SVG-first directories; a hit is usually clean.GOOGLE_CSE_KEY + GOOGLE_CSE_CX in ~/.secrets. Returns top URLs; download and validate it's actually an SVG.scripts/bg-strip.sh to remove the background. Set logo_bg_stripped: true and asset_strategy: site-raster-stripped (or whichever tier).SVG validation — after fetching anything claiming to be SVG: file must contain <svg, have a viewBox or width/height, and be > 200 bytes (filters out 1×1 tracking pixels). Strip embedded <script> tags for safety.
Background-strip rule for rasters — sample the four corner pixels: if they're a uniform color (all same RGB within ~5%) and not already transparent, run scripts/bg-strip.sh automatically. The script does ImageMagick flood-fill from each corner first (fast, deterministic, ~70% of cases); falls back to rembg (U²-Net model) for non-uniform / soft-edge backgrounds if installed.
Favicon cascade (separate from the trademark cascade — runs in parallel):
<link rel="apple-touch-icon" href="..."> from the homepage HTML — 180×180 polished square, designed for tile rendering. Best tier for chip/grid displays.<link rel="icon" href="..."> from homepage HTML — any size; biggest preferred./apple-touch-icon.png, /apple-touch-icon-precomposed.png, then /favicon.svg, /favicon-32x32.png, /favicon.png, /favicon.ico.<link rel="manifest">.https://www.google.com/s2/favicons?domain={d}&sz=128) — last-resort fallback that almost always returns something for resolvable domains.Run via scripts/favicon-hunt.sh <domain> — returns JSON with tier, url, format. ICO outputs need converting to PNG before use in pipelines that don't accept ICO (Astro's <Image>, etc.) — use magick "ico:input.ico" -resize 256x256\> output.png.
OG-image cascade (URL only, no download by default):
<meta property="og:image"> from homepage<meta property="og:image:url"><meta name="twitter:image"><meta name="twitter:image:src">Run via scripts/og-image-hunt.sh <domain> — captures a URL into the company's og_image_url frontmatter field. The URL is preserved for downstream consumers (a deck might pull it in for a hero slide; a portfolio detail page might use it as a banner). No bytes downloaded by default.
Output: portfolio/{co-slug}.md. Brand assets use the role-prefixed convention:
portfolio/trademark__{Company-Name}.svg (or .png fallback with alpha guaranteed if bg-strip ran)portfolio/wordmark__{Company-Name}.svg + portfolio/appIcon__{Company-Name}.svgportfolio/favicon__{Company-Name}.{svg|png} (ICO converted to PNG)og_image_url: (no file saved unless explicitly requested)Company-Name is Train-Case (e.g. Foundation-Health, 9am-Health, Inne). Full naming spec in schema/company.md → "Asset filename convention". The company's .md has separate logo: and favicon: fields pointing to their respective files.
Goal: for each portfolio company resolved in CP3, find the CEO and gather their metadata.
Cascade:
"CEO of {Company}" or "{Company} founder"/team or /about page if not LinkedInOutput: portfolio/{co-slug}-ceo.md (next to the company file).
| Tool | Type | Use for | When to prefer |
|---|---|---|---|
| Jina Reader | REST helper script (scripts/jina-reader.ts) | URL → clean markdown | Default first call when you just need a page's content. Fast, cheap, JS-rendering-aware. |
| Firecrawl MCP | MCP server | Structured extraction (URL + JSON schema → filled JSON) | When you need structured output (e.g., extract {name, title, bio, headshot}[] from a team page) or when Jina Reader returns thin content for a JS-heavy site. |
| Tavily MCP | MCP server | AI-native search — replaces "Google + scrape" | Advisor / CEO discovery, finding LinkedIn URLs by name+context, finding the right firm homepage from a name. |
| OpenGraph.io | REST helper script (scripts/og-fetch.ts) | OG metadata + image for any URL | Quick favicon / og:image / description without spinning up Firecrawl. |
| Bash + curl | built-in | Downloading assets to disk | After you've resolved the asset URL. |
| Brandfetch API | REST helper script (scripts/brandfetch.ts — to be added) | SVG logo + brand colors + fonts for a domain | Tier 4 in the logo cascade — when site SVG hunt fails. Free tier 1k/mo. Requires BRANDFETCH_API_KEY in ~/.secrets. |
| Tavily MCP (SVG repo search) | Tavily, with site: filters | Find SVG logos hosted on worldvectorlogo.com / seeklogo.com / vectorlogo.zone / wikimedia.org | Tier 5 in the logo cascade — when Brandfetch returns nothing. |
| Google Custom Search | REST (helper to be added) | fileType=svg web-wide SVG search | Tier 6 (last resort). Costs money beyond 100 free queries/day. Requires GOOGLE_CSE_KEY + GOOGLE_CSE_CX. |
ImageMagick (magick) | local CLI (brew install imagemagick) | Background removal via flood-fill from corners; also color sampling | Tier 1 of scripts/bg-strip.sh. Deterministic, no model deps, handles white + brand-color backgrounds. |
| rembg | local CLI (pipx install rembg) | AI-based background removal (U²-Net) for non-uniform / soft-edge logos | Tier 2 of scripts/bg-strip.sh. ~170MB model on first run, then offline. Optional — script gracefully degrades without it. |
Decision flow per URL:
Cache before call: every Jina / Firecrawl / Tavily / OpenGraph.io response is keyed on its URL or query hash and stored at ~/.claude/skills/crawl-fetch-ingest/cache/{firm-slug}/. Always check the cache before invoking the tool. Document the hashing convention in setup.md.
The skill's output schema is canonical and loosely enforced. Sites already have divergent shapes (mpstaton-site, fullstack-vc, hypernova-site, memopop-ai); adapting per-site happens on refactor, not on ingest. See:
schema/firm.md — firm-level frontmatterschema/person.md — CP1 + CP2 + CP4 (all people are the same shape, distinguished by role_class)schema/company.md — CP3Every file is .md with YAML frontmatter and a free-form body for prose bio / description / notes.
Every output file should have a confidence field in frontmatter: high, medium, low, or flagged. Use:
high — direct match on firm site or LinkedIn with all required fields filledmedium — match via search, plausible but not confirmed (e.g., common name)low — partial data, asset hunt failed, ambiguous matchflagged — should be reviewed before publishingNever silently drop an entity. If it appears in the deck and can't be resolved, write the file with status: unresolved + confidence: flagged and notes about what was tried.
When invoked:
sequoia-capital)./team, /portfolio) from URL if provided.cache/{firm-slug}/{tool}/. Check before calling.flagged entities, list of assets that fell back from SVG → PNG, anything the user should manually review.Subroutines are sub-workflows that operate on the output of the main four-checkpoint ingest. They are invoked after (or independently of) the main ingest, typically as quality-assurance or human-in-the-loop steps. Each lives as its own markdown file in routines/ with optional helper scripts in scripts/.
routines/triage-brand-assets.mdInvoke when: the user asks to "triage", "review", "classify", or "audit" the brand assets fetched for a firm. Typically run after CP3 + bg-strip + rename are complete, before publishing.
What it does: for each portfolio company, runs an auto-classifier (scripts/triage-classify.py) that scores the logo across existence / format / bg-strip success / foreground luminance / resolution / file size / flagged-status, then walks the user through each ambiguous case asking for one of good-to-go / not-urgent-passable / urgent-rework / deferred-for-now. Persists the choice as review_status in the company's frontmatter.
Output: updated frontmatter on each portfolio/{co-slug}.md plus an end-of-routine summary listing the urgent-rework items with suggested next moves (e.g., "try Brandfetch tier 4," "manual cleanup in Figma").
routines/investor-credibility-ingest.mdInvoke when: the user asks to "ingest our backers", "fill out the investor section", "do credibility ingest", "make these VCs legible", or supplies a list of firm names + the operating company they back. This is the company-anchored walk — root is the operating company, traversal goes outward through its named backers.
What it does: for each backer firm, runs CP1 (team across every role-bearing sub-page) + CP3 (portfolio + brand assets via the SVG cascade). Skips CP4 — at credibility-card distance, portco CEOs add no legibility. Writes to <cwd>/data/investors/{firm-slug}/ so multiple backers coexist alongside the operating company's own data/team/.
Output: data/investors/{firm-slug}/firm.md + team/{person}.md + team/{person}.jpg + portfolio/{co}.md + portfolio/trademark__{Co}.svg + portfolio/favicon__{Co}.png per backer firm. End-of-routine summary covers per-firm team/portfolio counts, SVG-vs-raster asset success rates, and cross-firm portfolio overlaps (co-investments are a credibility multiplier the rendering layer may want to highlight).
The pattern: drop a new routines/{name}.md with frontmatter (name, description), then add a one-paragraph entry in this section that tells the agent when to invoke it. Helper scripts go in scripts/{name}.py|.sh|.ts and are referenced from the routine doc, not from SKILL.md directly. This keeps SKILL.md as a stable map and lets subroutines evolve independently.
Examples of future subroutines that fit this pattern: triage-person-assets.md (same idea but for headshots — wrong person, low-res, missing), re-fetch-flagged.md (re-run the cascade with stricter parameters on entities marked confidence: flagged), export-to-site.md (per-site adapter that converts data/firms/... into a target site's content-collection schema).
The skill writes only under <cwd>/data/ — data/firms/{firm-slug}/ (firm-anchored), data/investors/{firm-slug}/ (company-anchored, via investor-credibility-ingest), or data/team/ + data/portfolio/ (flat operating-company variant). Wiring any of these outputs into a specific site's content collection (e.g., mpstaton-site's src/content/team/) is a separate, per-site task. That separation is intentional — sites diverge in schema, asset paths, and routing; the skill stays neutral.
setup.md — one-time install (.secrets, MCP servers, npm packages, ImageMagick, rembg)schema/{firm,person,company}.md — canonical frontmatter (incl. logo asset_strategy fields)scripts/jina-reader.ts — URL → markdownscripts/og-fetch.ts — OpenGraph.io REST wrapperscripts/brandfetch.ts — Brandfetch API wrapper (tier 4 of the logo cascade); --best-svg / --best-raster / --save-all modesscripts/bg-strip.sh — auto-strip backgrounds from raster logos (ImageMagick → rembg cascade)scripts/logo-hunt.sh — run tiers 1–3 of the trademark/wordmark cascade locally for a domainscripts/favicon-hunt.sh — run the favicon cascade for a domain (apple-touch-icon → site icons → Google S2 fallback)scripts/og-image-hunt.sh — extract og:image / twitter:image URL from a homepagefuture-work.md — PDF OCR (port from memopop-orchestrator), Google CSE wrapper, design-system-viewer-style preview UI~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.