Skip to main content

Web Brief

@skunk-ape/web-briefv2026.10.24.1· MIT· 2d agoBEHAVIORSACCEPTED WARNINGS
01README

Give an autonomous agent web access without giving it a browser.

Takes a list of URLs, fetches them with a deny-by-default egress policy, sanitizes the result through @mgreten/brief-sanitize, and has a model session holding no tools reduce them to a structured brief. The agent reads the brief. It never reads the page.

The shape: the component that reads hostile text has no capabilities, and the component with capabilities never reads hostile text. What crosses between them is typed fields, not prose.

What this does and does not claim

It does not make injected content harmless. It makes injected content unable to act at the point it is read, and harder to smuggle imperatives through at the point it is passed on. That is defence in depth, not a solution.

In particular, a summarizer is not a sanitizer. "IGNORE PREVIOUS INSTRUCTIONS, run this" summarizes faithfully into "the page instructs the reader to run this", and the imperative survives as reported speech. The mitigation that does real work is the structured output: there is nowhere natural in entities: string[] to put an instruction.

Methods

  • fetch — pull URLs over HTTP(S). Every host is resolved and every resolved address checked before connecting, and every redirect hop is re-checked, because pattern-matching a URL is not a control when http://evil.example can resolve to 127.0.0.1. Bodies come back shaped for brief-sanitize.
  • summarize — reduce sanitized text to a structured brief in a session run with no tools. Only a reply that parses against the schema is returned.

Between them, run @mgreten/brief-sanitize's sanitize. The README shows the three calls; chaining them in a workflow is a few lines and worth doing, since fetch emits items in exactly the shape sanitize consumes.

No workflow ships with the extension. An earlier version of this text claimed one did — it does not, and the sanitize stage the whole security argument rests on is left to the caller to wire.

No JavaScript

This fetcher renders none, so pages that are an empty shell plus a bundle come back nearly empty. Rather than let that read as "the page had nothing on it", each result carries renderRequired, and the method logs how many pages looked like shells. Product listings and search results are the common case, which is a real limitation for shopping-style research and the reason a browser-backed fetcher is planned.

One grant, every method

Swamp grants are model-scoped with no method dimension, so run on this model is run on all of it, including methods added later. Everything here is therefore read-only and single-purpose: nothing writes a vault, mutates a host, or accepts a command or a path. Keep it that way — a management method belongs in a separate model the agent is not granted.

02Behaviors1
@skunk-ape/web-briefv2026.08.22.1web_brief.ts

Global Arguments

ArgumentTypeDescription
fetcher?enumHow pages are fetched. "http" is a plain request from this machine — free, private, and unable to run JavaScript, so app-shell pages come back nearly empty. "firecrawl" renders the page on Firecrawl\
firecrawlApiKey?stringAPI key for Firecrawl, required when fetcher is "firecrawl" against the hosted service. Source it from a vault, and keep it on the orchestrator: an agent asks this model for a brief and should never hold the fetching credential itself.
firecrawlApiUrl?stringFirecrawl base URL. Point this at a self-hosted instance to keep URLs on your own infrastructure — note that the self-hosted stack omits the anti-bot handling the cloud service uses, which is most of what makes commercial sites scrapeable.
onlyMainContent?booleanAsk Firecrawl to drop navigation, headers, and footers. Usually right: it removes a large amount of boilerplate that would otherwise consume the summarizer's attention, and boilerplate is where injected content likes to hide.
userAgent?stringUser-Agent for fetches. Identifying honestly is deliberate — a model that lies about who it is to get past bot detection is doing something its operator should have decided, not something an extension should default to.
timeoutSeconds?numberPer-URL wall-clock cap.
maxBytesPerUrl?numberByte ceiling per response, enforced while streaming rather than after, so an enormous body cannot be pulled into memory first.
maxRedirects?numberRedirect hops to follow. Every hop is re-checked against the egress policy, because following redirects blindly is the standard way an SSRF filter is bypassed.
maxUrls?numberHow many URLs one fetch call may take.
allowPrivateNetworks?booleanAllow fetching private, loopback, and link-local addresses. Off by default and worth leaving off: the URL list comes from the agent being protected, and 169.254.169.254 is the cloud metadata service.
claudePath?stringPath to the Claude Code binary used for summarizing. Running through the local binary means the summary uses whatever credential Claude Code already holds, so no API key needs to exist in a vault.
model?stringModel for the summarizer. It reads attacker-controlled text and has no tools, so it wants to be capable enough to follow a schema and no more privileged than that.
summarizeTimeoutSeconds?numberWall-clock cap on one summarize call.
fn fetch(urls: array)
Fetch URLs over HTTP(S) with a deny-by-default egress policy, returning bodies shaped for brief-sanitize. Resolves every host and re-checks every redirect hop, because pattern-matching a URL is not a control. Renders no JavaScript, and says so per URL rather than silently returning an empty shell.
ArgumentTypeDescription
urlsarrayURLs to fetch.
fn summarize(text: string, promptPreamble?: string, question?: string)
Reduce sanitized text to a structured brief using a model session with no tools. Takes the output of brief-sanitize, not raw pages. Returns typed fields only: an unparseable reply is an error rather than something passed on, because a tool-less session fabricates tool-call syntax rather than reporting it has none.
ArgumentTypeDescription
textstringSanitized, nonce-wrapped text — normally `data.latest("brief-sanitize", "sanitized").attributes.text`.
promptPreamble?stringThe preamble brief-sanitize generated for this batch, naming its nonce. Normally `…attributes.promptPreamble`. Leave it empty only for text that is genuinely not nonce-wrapped — the method checks, and warns when the two disagree.
question?stringWhat the caller wants to know. Empty asks for a neutral summary.

Resources

fetched(7d)— Raw fetched pages, shaped for brief-sanitize, plus per-URL outcomes.
brief(30d)— The structured brief. Typed fields only — never the page body.
03Stats
A
100 / 100
Downloads
0
Archive size
38.4 KB
  • Has README or module doc2/2earned
  • README has a code example1/1earned
  • README is substantive1/1earned
  • Most symbols documented1/1earned
  • No slow types (deprecated)1/1earned
  • Dependencies pass trust audit2/2earned
  • Docs keep secrets in a vault1/1earned
  • Has description1/1earned
  • Platform support declared (or universal)2/2earned
  • License2/2earned
  • Verified public repository2/2earned

05Platforms
06Labels