shrwnsan/search-plus
v3.5.2Apache-2.0
Enhanced web search with multi-service fallback (TinyFish, Tavily, Brave, Exa, Jina, plus keyless Firecrawl/Defuddle) — works without API keys
Changelog
All notable changes to the search-plus plugin will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[3.5.2] - 2026-11-03
Changed
- Skills sync to the shrwnsan/agents hub is now gated on a fail-closed portability allowlist (
.github/sync-skills.json; no entry = no sync). search-plus opts in — meta-search keeps syncing as before.
[3.5.1] - 2026-10-29
Fixed
- URL validator no longer rejects legitimate
.com.<ccTLD>second-level domains such asamazon.com.auorcarousell.com.hk(#87). The malformed-TLD check now tests the hostname with a 3+ letter qualifier instead of the full URL string, so a.com.appearing in a query string no longer triggers it either. - Removed a redundant unanchored
example.comURL pattern that rejected any host merely ending in the substringexample.com(e.g.notexample.com); reservedexample.com/net/orghosts are still rejected by the hostname check. - Offline regression tests for the suspicious-domain gate: ccTLD hosts pass, malformed
.com.<3+ letter>TLDs and reserved test domains stay rejected.
[3.5.0] - 2026-10-23
Added
- TinyFish search and fetch provider: set
SEARCH_PLUS_TINYFISH_API_KEY(free key, no card) to add TinyFish Search as the first keyed search provider (after SearXNG, before Tavily), and TinyFish Fetch, which renders pages in a real browser, after Jina Reader and before Firecrawl in the extraction chain. Both are free within TinyFish's daily allowances; a 402 (allowance used), 429, or per-URL error such asbot_blockedfalls through to the next provider. - Search order chosen from a head-to-head eval (eval-028): TinyFish put the official source first on 23 of 29 queries, against 3–5 for Tavily. Tavily still runs as the fallback and is the only provider that adds a synthesized answer.
Changed
- A Tavily search failure now logs its error message, like the other providers.
TINYFISH_API_KEYis accepted as a fallback whenSEARCH_PLUS_TINYFISH_API_KEYis not set, matching the other providers (a deprecation notice points to the prefixed name).
[3.4.0] - 2026-10-15
Added
- SearXNG search provider (PRD-009): set
SEARCH_PLUS_SEARXNG_URLto your own instance to try it first for search; optionalSEARCH_PLUS_SEARXNG_API_KEYis sent as a Bearer token. A 403 response explains that JSON output must be enabled in the instance'ssettings.yml. - Offline test coverage for every search provider (PRD-009 F3): recorded, sanitized responses with stubbed
fetchcheck each provider's endpoint, auth, normalization, and fallback order. - Docs-sync test (PRD-009 F4): fails when a provider host is missing from SKILL.md
allowedDomainsor aSEARCH_PLUS_*variable is undocumented.
Fixed
- Placeholder content no longer counts as success (PRD-009 F2): parked-domain pages, blank bodies, and provider error bodies returned with HTTP 200 now fail that attempt, so the extraction chain moves to the next provider and the attempt shows as rejected under
Tried:. Generic words such as "forbidden" or "rate limit" are not treated as failures, because real articles contain them. - Tavily search with zero results now falls through to the next provider, like the other providers.
[3.3.1] - 2026-10-15
Security
- Direct markdown fetch could reach internal addresses (PRD-009 F1): redirects are now followed manually (max 5 hops) and every hop is re-validated, so a public URL can no longer bounce the request to a loopback or private address. The private-address blocklist now also covers
0.0.0.0/8,100.64.0.0/10, and IPv6 loopback/unspecified, unique-local, link-local, and IPv4-mapped addresses; bracketed IPv6 literals in URLs are classified correctly. - DNS rebinding on the direct fetch is closed: the direct fetch now uses its own
http/httpstransport whose DNS resolution validates every address at connect time — inside the resolver call the connection itself uses, honoring both the single-address and Node ≥ 20autoSelectFamily(all: true) contracts. A short-TTL answer can no longer hand the socket a private address the pre-flight check never saw. - The blocklist now covers the remaining IANA special-purpose ranges: IPv4 protocol assignments (
192.0.0.0/24), TEST-NET-1/2/3, benchmarking (198.18.0.0/15), multicast (224.0.0.0/4), and reserved/broadcast (240.0.0.0/4); IPv6 IPv4-compatible (::/96), 6to4 (2002::/16), NAT64 (64:ff9b::/96), and Teredo (2001:0::/32) are checked through their embedded IPv4, plus documentation (2001:db8::/32) and multicast (ff00::/8). Subdomains and trailing-dot forms oflocalhostare rejected before DNS resolution. - Direct-fetch bodies are size-capped at 2 MiB of decoded bytes (gzip/deflate/br responses are decompressed first so a compression bomb cannot slip past), and a body declaring a larger
Content-Lengthis refused before any byte is downloaded.
Changed
- The direct fetch checks status and
Content-Typebefore downloading the body, so non-markdown pages are no longer fully downloaded and discarded. - The direct fetch sends a
User-Agent(previouslyfetch's implicit one) so UA-filtering origins do not silently waste the chain head, and transport errors keep their code (ETIMEDOUTetc.) when the underlying error message is empty. - The SSRF regression suite runs fully offline: redirect hops, DNS (including a rebinding scenario), and the transport are stubbed, so the DNS-dependent skip is gone.
3.3.0 - 2026-10-08
Added
- Direct markdown fetch at the head of the URL extraction chain (#99): before any third-party service, the origin is asked for markdown (
Accept: text/markdown). Trusted only on 200 +text/markdown+ non-empty body; anything else falls through silently to the existing chain with the attempt visible in theTried:output. Always-on, no configuration, one round trip for sites that honor content negotiation.
Fixed
github-service.mjsPromise.racetimeout timers are now cleared on completion (#98)- Wayback availability-API outages are reported as such instead of "No Wayback snapshot available" (#98)
- Reserved test domains (example.com/net/org) are rejected with an actionable "reserved test domain" message instead of the generic suspicious-domain wording (#98)
agents/search-plus.mddocuments the real CLI output contract instead of a schema with fields nothing produces (#96)
3.2.0 - 2026-10-08
Removed
- Legacy recovery machinery (~1,000 lines):
handle-search-error.mjsandhandle-rate-limit.mjswere unreachable dead code — the only error that could reach their dispatcher was the fixed all-failed message, against which every branch was inert (the 879-line file behaved as a stderr string prefixer). The rate-limit path slept 60s + 120s against the hook's 25s deadline, and its retry-after parsing read a field no provider error carries.SEARCH_PLUS_RECOVERY_TIMEOUT_MSandSEARCH_PLUS_451_SIMPLE_MODEare gone with them (SEARCH_PLUS_404_MODEremains, still live in content extraction). scripts/test-451-recovery.mjs(repo root): already broken — imported a path that no longer exists; not referenced by CI.
Fixed
- Content-validation false signals: Jina Reader frontmatter keys (
URL Source:,Markdown Content:) no longer blanket-reject all Jina output; Wayback banner chrome no longer fails validation for wayback-sourced content (every snapshot embeds it); parked-domain "buy this domain" spam now fails validation instead of passing as page content. - Jina removed from the Wayback snapshot extractors: r.jina.ai is blocked for web.archive.org until 2035, making the second extractor provably dead weight.
Changed
- Web-search all-failed stderr message no longer carries the
Search failed:prefix previously added by the deleted recovery dispatcher.
3.1.0 - 2026-10-07
Fixed
- Keyless URL extraction was broken: spoofed Chrome 91 headers made
r.jina.aireturn a Cloudflare 403 challenge; extraction no longer sends browser headers to API services - Hook output was never applied: progress logs went to stdout, so Claude Code could not parse the hook JSON. Stdout now carries only the JSON (logs go to stderr with
SEARCH_PLUS_DEBUG=1) - Hook false positives: recovery fired on successful pages that mentioned "403"/"429"/"empty". Detection now uses the tool's status code and result structure
- Failures reported as success: failed URL extraction exited 0 and printed ~50 KB of failure JSON. It now exits 1 and lists each service tried
- Wayback Machine fallback never ran (
cacheURL.substring is not a function) - Tavily requests left a 15s timer running after completion (
clearTimeouton a promise)
Added
PostToolUseFailurehook for WebSearch/WebFetch: tool errors (e.g. a WebFetch 403) are now recovered, not only successful-but-empty results- Firecrawl search and scrape: keyless by default, optional
SEARCH_PLUS_FIRECRAWL_API_KEY. Restores web search with no API keys - Defuddle (
defuddle.md) keyless URL→markdown extraction fallback - 25s internal deadline in the hook so it exits cleanly before the 30s hook timeout
SEARCH_PLUS_DEBUG=1for progress logs
Changed
- Script output is compact markdown (title, source, content / numbered results) instead of raw JSON. Hook context is capped below Claude Code's 10,000-character limit
- The per-call service health check is now opt-in; it spent a Tavily credit and Jina tokens on every extraction
allowed-toolsin SKILL.md uses Claude Code tool names (WebSearch,WebFetch)
Removed
- Dead fallbacks: Google Cache and Bing Cache (both shut down), Yandex Turbo, "double/triple" Jina redirects, a malformed "textise" URL, and Tavily retries that only changed the User-Agent sent to the Tavily API
3.0.1 - 2026-04-16
Fixed
- Error recovery now uses all 4 providers (
performHybridSearch) instead of Tavily-onlycontentExtractor.tavily.search()— 12 call sites fixed acrosshandle-search-error.mjsandhandle-rate-limit.mjs
Removed
- ~250 lines of dead code:
trySearXNGSearch(),tryDuckDuckGoHTML(),tryStartpageHTML()functions, dead transformers, and dead service bonus entries - Unused
security-utils.mjsimport
Changed
- Updated
STANDARD_RESPONSE_FORMAT.mdto document current providers only - Updated README sandbox guidance with root cause and
excludedCommandsworkaround
3.0.0 - 2026-04-14
Added
- Brave Search API provider — independent 30B+ page index, fastest latency (~670ms avg),
SEARCH_PLUS_BRAVE_API_KEY - Exa AI Search provider — neural/semantic search for technical docs (~1.2s avg),
SEARCH_PLUS_EXA_API_KEY - Jina Search API provider (
s.jina.ai) — replaces dead free services,SEARCH_PLUS_JINA_API_KEY - 4-service sequential fallback chain: Tavily → Brave → Exa → Jina Search
- Response transformers for all new providers (brave, exa, jina-search)
- Service reliability bonuses for relevance scoring: Brave +0.09, Exa +0.08, Jina Search +0.07
- Environment variable deprecation warnings for old naming conventions
- Sandbox compatibility documentation
Changed
performHybridSearch()replaced deadPromise.any([SearXNG, DuckDuckGo, Startpage])with sequential 4-service chain- Env var naming standardized to
SEARCH_PLUS_prefix (old names still work with deprecation warnings) - Jina env var canonical name changed from
SEARCH_PLUS_JINAAI_API_KEYtoSEARCH_PLUS_JINA_API_KEY - Plugin description updated to reflect multi-service architecture
Removed
- BREAKING: Free keyless web search path — was dead anyway (100% failure rate since early 2026)
- SearXNG public instances: all return 403/429/timeout or are offline
- DuckDuckGo HTML: returns CAPTCHA on all requests
- Startpage HTML: returns JS-only shell with 0 parseable results
Not Affected
- URL extraction via Jina Reader (
r.jina.ai) — still works without API key at 20 RPM - GitHub content extraction via
ghCLI — unchanged
2.11.0 - 2026-04-12
Added
- Sandbox compatibility notes in SKILL.md
- PostToolUseFailure limitation documentation
2.10.1 - 2026-04-11
Previous versions were not tracked in a CHANGELOG.