
Groundhog
io.github.dmytrome由 GitHub 帳號 dmytrome 發布v0.22.0更新於 Oct 26, 2026
Search, read and research the web, with hidden text stripped by default and sources hashed
發布方自己的服務:groundhog.markets
概覽
Groundhog 讓助理透過真實 Chrome 搜尋網頁、把頁面讀成乾淨的 Markdown,並進行多來源研究,預設剝除並回報隱藏文字。
- 可以做什麼
- 搜尋網頁,回傳標題、連結、摘要、引擎與日期等排序結果,不抓取頁面
- 把網址讀成 Markdown 或純文字,可依查詢做段落排序、控制 token 預算、依頁拆分 PDF,並附來源憑證
- 執行研究流程:搜尋、讀取排名前面的來源並跨來源排序段落,支援網域與日期過濾,並提供略過內容的收據
- 跟進回傳段落中提供的候選連結,繼續讀取更多頁面
- 檢查瀏覽器端點是否可連線,無法連線時給出提示
- 適用情境
- 適合助理需要讀取會阻擋一般抓取器的真實網頁,或需要防範頁面隱藏文字造成的提示注入風險、希望取得剝除內容與移除報告的情境。也適合需要逐來源憑證與過濾紀錄的多來源研究。
- 執行需求
- 透過 uvx 在本機執行(Python)。需要已安裝的 Chrome,或用於 stealth 容器的 Docker/Podman,或透過 CDP_URL 指定的 CDP 端點。搜尋可選用 SEARXNG_URL,未設定時透過瀏覽器算繪搜尋頁。不需要 API 金鑰或帳號。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Groundhog,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
Groundhog
Web search, read and research for AI agents, through a real, stealth-patched Chrome. Groundhog is an MCP server that returns clean Markdown a model can trust: text no human could see is stripped before the model reads it and reported, every source comes with a provenance receipt, and a real browser reads pages that block plain fetchers.
Measured against other fetch layers
Twenty-eight pages carry text a human can't see, and three controls carry none. A case passes when the payload doesn't reach the model and the article text still does.
Other extractors often drop hidden text by accident, but none of them tells the caller
anything was removed. The three cases Groundhog doesn't report (a hidden input value, a meta
description, an iframe srcdoc) never reach any extractor. The corpus is public at
dmytrome.github.io/groundhog; the harness and its
limits are in benchmark/.
Quick start
Claude Code:
Claude Desktop, Cursor, Windsurf:
On the first fetch Groundhog starts the Chrome you already have, headless, in its own
profile at ~/.groundhog/chrome. Your own browsing, tabs and logins are never touched. If
Chrome is missing, status and the error say what to install.
GROUNDHOG_BROWSER=stealth runs the hardened browser container instead (Docker or Podman;
the first pull takes a few minutes). To run it yourself:
CDP_URL points Groundhog at any browser and is used as given: http:// is probed for
/json/version, ws:// or wss:// (what hosted browsers hand out) is connected to directly.
Auto-start only runs for a local address that isn't answering, and
GROUNDHOG_AUTO_START_BROWSER=false turns it off. From source: cd mcp && uv sync && uv run groundhog-mcp.
All four tools are annotated readOnlyHint: they write nothing and change no remote state.
The one side effect is auto-start, which may launch Chrome or, with stealth, run the
container after removing an unreachable one named groundhog-browser.
What makes it different
- Hidden text is stripped and reported. A rendered DOM shows what a human would see;
everything else goes, and each removal is listed in
threats. It's a strong heuristic, not a proof: see the limits. - Every source carries a receipt: a SHA-256 of the extracted content, canonical URL, language, word count, and author and date when the page declares them.
- Safe by default. An SSRF guard checks every document request, redirects and frames included, with per-domain rate limiting. See Security.
- No automation tell. Groundhog drives Chrome over raw CDP and never enables the
Runtimedomain that anti-bots detect in Puppeteer, Playwright and Selenium. - A real fingerprint. Real Chrome means real TLS, HTTP/2, WebGL and canvas. In headless
mode the
HeadlessChrometoken is replaced and client hints are copied from the browser; shared and service workers aren't covered. The stealth image runs headful under Xvfb. - No model, no API key.
researchreturns extracts and your agent does the synthesis.
Tools
read_url(url, format="markdown", max_tokens=None, query=None, include_hidden=False)
Stripped by default: display:none, visibility:hidden, content-visibility:hidden,
opacity ≤ 0.05, font-size < 4px, zero-size and display:contents boxes that render
nothing, .sr-only-style sub-pixel boxes, clip: rect(...), a clip-path that leaves
nothing (circle(0), inset(100%), a polygon on one line), transform: scale(0) on either
axis, a filter whose opacity is 0.05 or less, zero-height or zero-width boxes that clip
their overflow, transparent or background-coloured text, off-page positioning, collapsed
<details>, <template> content, and zero-width, bidi and Unicode Tag characters. A box
whose only text is an open popover or modal dialog is kept, because that dialog still shows
on screen; any other text in the box is stripped as usual. HTML comments are reported too.
In a PDF, text drawn in render mode 3 or 7, which paints nothing, is stripped and reported
as hidden_pdf. That includes the OCR layer of a scanned page, so a scan whose only text is
that layer is empty unless you pass include_hidden.
include_hidden=True keeps the text but still reports it. alt, aria-label,
aria-description and title text never appears in markdown or the rendered text; values
of 125 characters or more are reported as hidden_attribute with a capped excerpt, because
long text there is a known carrier.
Treat threats as untrusted: its excerpts are page-authored text, cleaned and length-capped.
At most 50 findings per page are returned (10 per source in research), with the two classes
capped separately so decoys can't bury a real payload. Notices are appended after the cap.
query swaps head-truncation for BM25 passage ranking on the sanitized content, keeping
passages within half the best score up to the budget.
search(query, limit=10)
Returns hits (title, url, snippet, engine, score, published) and the backend
that answered. Nothing is fetched. With SEARXNG_URL set it uses your
SearXNG instance, which needs formats: [html, json]; otherwise
it renders a search page through the browser. Hit text is cleaned and capped, and a hit whose
URL would change under cleaning, isn't http(s), carries credentials or exceeds 2048
characters is dropped. A backend that can't answer raises an error instead of returning an
empty web.
research(query, max_sources=5, max_tokens=None, include_domains=None, exclude_domains=None, published_after=None, aim=None, follow=None)
Searches, reads the top sources and ranks passages from all of them in one pass.
passages:id,text,source_url,heading,score,start,end,clamped,fence.textis exactlymarkdown[start:end];clampedsays it was cut short to fit the budget. A code block is never cut short, only left out. A passage cut from a long code block carries the block's opening line infence: reopen with it whentextdoesn't start with it, and close the block whentextdoesn't end with a closing fence.idstays stable while the page doesn't change.sources:url,title,status(ok/blocked/timeout/error),page_status(as inread_url),threats,threat_total,threat_counts(every finding by type, spans the page's detector dropped ashidden_unclassified),provenance,filtered,depthanddiscovered_via(the candidate it was followed from, with yourwhy). A failed source doesn't fail the call. A challenge, an error page, anemptypage or any page whosemarkdownis blank counts asnot_contentand contributes no passages.- One page per registrable domain unless
include_domainsis set, and then no more pages per site thanGROUNDHOG_MAX_CONCURRENT_PAGES;max_sourcesgoes up to 10.
include_domains reads only those domains and their subdomains, exclude_domains never
reads them, and published_after (YYYY-MM-DD) drops pages dated earlier. Up to 8 domains
can be included and 50 excluded. A page that redirects into an excluded domain or out of the
included ones, is dated too early, or repeats the URL or text of a page already used is listed
with filtered and contributes no passages. The query sent to the engine names at most eight
domains; further excluded ones are filtered after the search. aim (up to 200 characters)
says what the evidence must show and is echoed back without changing the search or the
ranking.
receipt holds the executed_query, the distinct hits and hits_by_domain, the hits not
fetched (hits_excluded, hits_outside_domains, hits_before_date, hits_same_domain,
hits_unread past the source limit), each source counted
once as failed, not_content, filtered or usable, outside_domains, unknown_dates,
and the passages omitted, set_aside or clamped. Read it before concluding the web has
no answer.
candidates are links inside the returned passages, at most 12, no more than 4 from one
page and 3 to one domain. Each has a token, url, domain, label (the link text, which
the page wrote), parent_source_url, parent_passage_id, depth and flags
(parent_strip_incomplete, parent_detection_degraded, parent_threats=N,
likely_unsupported_format, same_domain_pages=N). Links back to the page, to pages already
read in the call, outside the domain filters, or from a page three links deep are not
offered. To read some, call again with follow=[{"token": ..., "why": ...}] (up to 8, each
why up to 160 characters) and the same query and filters: nothing is remembered between
calls. A follow runs no search, and followed says for each entry whether it was read,
not_content, failed, filtered, a duplicate, over the same_domain_limit (as many
pages per site as can start before the deadline), an invalid_token or expired. Tokens are
signed by the server and expire after 30 minutes or on restart.
The JSON text block of a response never exceeds 40,000 characters; the MCP result carries the
same object again as structuredContent. When it would, cards go first, then threat
excerpts, then passages, then metadata, each counted in the receipt (candidates_omitted,
threat_excerpts_omitted, passages_omitted, sources_omitted, metadata_compacted);
safety counts from sources left out are kept in omitted_safety, and notes says so:
read_url on a source returns its page, and a narrower query or fewer sources leaves more
room.
status()
Returns browser_reachable, cdp_url (scheme, host and port only) and a hint when the
browser can't be reached.
Configuration
MCP server:
Browser container:
The stealth Chrome container
Headful Chrome under Xvfb with a CDP endpoint, usable by any CDP client. It reports Chrome
rather than HeadlessChrome, sets navigator.webdriver to false, and sets the UA at launch.
With PROXY, timezone and locale match the exit IP, credentials go through a local tinyproxy,
and WebRTC is pinned to the proxy. WebGL uses a detected GPU, else Mesa llvmpipe.
Against a fresh container (Chrome 151, no proxy), over raw CDP:
iphey flags any datacenter IP, so it's tracked but not scored. The live
table is in RESULTS.md. Puppeteer, Playwright and Selenium are still flagged
because they enable Runtime; examples/ has clients for each, and
OTHER_TOOLS.md covers more.
Security
The CDP endpoint is unauthenticated: anyone who reaches the port controls the browser. Keep it
on localhost or a private network. The stealth image runs --no-sandbox, so keep the
container isolated. Report vulnerabilities via SECURITY.md.
The SSRF guard resolves each host and refuses loopback, private, link-local (including
169.254.169.254), reserved, multicast, unspecified, CGNAT and IPv4-mapped IPv6 addresses,
non-http(s) schemes, and URLs with credentials. It checks every document request Chrome is
about to send, redirects and cross-site frames included, and final_url once more.
Sub-resources are left to Chrome's Local Network Access rule, and the Chrome that Groundhog
launches has preloading turned off.
Limits of the guard:
- Only documents are checked. An older Chrome, or a site granted local-network access in the profile, isn't covered for sub-resources.
- Preloading stays on in the stealth container and in a browser you supply.
- Groundhog and Chrome resolve DNS separately, so a short-TTL rebinding window remains.
- Fetches share one profile, so cookies set by one page persist into later fetches, and page JavaScript can make its own requests from it.
GROUNDHOG_BLOCK_PRIVATE_IPS=false turns the guard off; use it only where reaching internal
addresses is intended.
Limits of hidden-text detection
An empty threats list isn't a clean bill of health. Detection runs in an isolated world the
page can't tamper with, and the live page is never modified. But:
- Thresholds can be sat just inside (
opacity: 0.06,transform: scale(0.01)), and a style is only treated as hiding when it provably paints nothing, so these pass:text-indent,inset(50%)(Chrome rounds the value it reports),clip-pathin pixels or byurl()/path(), 3D rotations,mask-image, text painted over by another element or stacked behind one, and a positioned child placed outside a box that clips its overflow. - In a PDF only render modes 3 and 7 are detected. White or tiny text, text outside the page,
hidden layers and text under an image pass, and every PDF result says so in a
detection_degradednotice. A PDF whose invisible text is in a font whose glyph widths can't be measured (vertical writing, a CMap other thanIdentity-H) isunsupported_contentrather than returned with that text in it, unless you passinclude_hidden. Parsing runs in a child process that is killed at 20 seconds. On Linux it's also held to 1 GiB of memory, and elsewhere only the time cap and a 256 MiB cap on decoded streams bound it. - The invisible-character set is a denylist.
- Closed shadow roots can't be read, so their content is left out entirely.
- When the page wins the cascade against the hiding stylesheet or hides its own
<body>, text is rebuilt from markup, line breaks are guessed, andstrip_incompletesays so.
Limits of block detection
- A challenge is called with certainty from vendor headers (
cf-mitigated,x-vercel-mitigated,x-amzn-waf-action,x-dd-b,x-datadome-cid) or challenge assets (Cloudflare, DataDome, PerimeterX, Imperva). Other vendors fall back to English wording on a near-empty page. - A soft block, a 200 with the data quietly withheld, looks like content.
unknownisn'tok, but it isn't treated as a block either.- Vendor presence (
server: cloudflare,cf-ray) is deliberately ignored.
A note on "stealth"
Best effort. It beats common open-source detectors, not commercial anti-bot systems that gate on IP reputation and behaviour. Use it for legitimate, authorized automation.
Privacy Policy
Groundhog is self-hosted software and talks to no servers the project runs. It collects nothing: no telemetry, analytics, crash reports or accounts.
Pages are fetched by a browser on your machine and returned to your MCP client. The server
keeps no database and writes no logs. The browser profile does persist cookies and storage:
~/.groundhog/chrome for local Chrome (delete the folder to discard it), or the container for
stealth (docker rm -f groundhog-browser).
Your traffic reaches the sites you fetch, your search backend (your SearXNG, or a public search
engine without one), an IP-geolocation service for the proxy's exit IP only when PROXY is
set, and PyPI and GHCR at install time.
Questions: SECURITY.md or https://github.com/dmytrome/groundhog/issues.
License
來源:README.md,提交 7fed218
工具
0版本歷史
13- v0.22.0最新Oct 26, 2026
- v0.20.1Oct 25, 2026
- v0.20.0Oct 25, 2026
- v0.19.1Oct 23, 2026
- v0.19.0Oct 23, 2026
- v0.18.2Oct 22, 2026
- v0.18.1Oct 22, 2026
- v0.18.0Oct 21, 2026
- v0.17.2Oct 21, 2026
- v0.17.1Oct 5, 2026
- v0.17.0Oct 2, 2026
- v0.16.1Sep 19, 2026
- v0.15.0Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分