Standalone hook-tool project for Claude Code that augments two built-in tool paths at the client runtime layer:
WebSearchWebFetch
This project is intentionally designed for Claude Code only. It relies on Claude Code hook events such as PreToolUse, so it should be treated as a Claude Code runtime integration rather than a general gateway/server feature.
This project solves a practical gap that appears when Claude Code is used with custom endpoints or third-party model paths.
In those environments, Claude Code may still emit native WebSearch or WebFetch intent, but the upstream path may not support the same server-side search/process behavior that Claude’s native stack expects.
As a result, users can hit problems such as:
- native
WebSearchnot working correctly through a custom endpoint - provider/model paths that cannot complete the expected Claude Code search process server-side
WebFetchreturning template-heavy or CSR-heavy HTML that is technically reachable but not actually usable as readable content
This tool increases the practical usefulness of Claude Code in two ways:
- adds a search substitution path when server-side/native search processing is not available through the custom endpoint
- preserves native Claude Code behavior when the custom path should not take over
- avoids dead-end failures by falling back instead of trapping the user in a broken custom search path
- adds a pre-check layer before fetch execution
- distinguishes fetch-readable pages from template-heavy or browser-render-required pages
- escalates to scraper fallback only when needed
- preserves native fetch when the simpler path is already good enough
- Supports multiple search providers
- Current built-in providers:
- WebSearchAPI.ai
- Tavily Search
- Exa Search
- Falls through to native Claude Code WebSearch when custom execution should not take over
- Supports API key pools, file-based keys, and automatic next-key fallback
- Supports provider policy modes such as
fallbackandparallel
- Probes the initial HTML first
- Allows native WebFetch for fetch-readable pages
- Detects template-heavy / portal-heavy / browser-render-required pages
- Uses one selectable extraction backend per request when extraction is needed
- Supports ordered fallback across:
- WebSearchAPI.ai Scrape
- Tavily Extract
- Exa Contents
- Falls through to native WebFetch when custom execution is unavailable or unsuccessful
Current implementation status:
- WebSearch supports:
- WebSearchAPI.ai
- Tavily Search
- Exa Search
- WebFetch supports:
- WebSearchAPI.ai Scrape
- Tavily Extract
- Exa Contents
Current direction:
- the project keeps the same Claude Code hook model while expanding both search and extraction provider support
- provider choice can evolve without changing the core Claude Code hook entrypoints
- in other words, the project has moved from provider-specific implementation toward provider-agnostic architecture
The current planning context for WebSearchAPI.ai is:
| Plan | Price | Search Credits | Notes |
|---|---|---|---|
| Free | $0/mo | 2,000/mo | Basic search capabilities |
| Pro | $189/mo | 50,000/mo | More search power for growing usage |
| Expert | $1250/mo | 500,000/mo | Higher-scale usage, custom rate limits, volume discounts, SLAs/MSAs |
Common capability notes from the current plan snapshot:
- content extraction
- localization (country and language)
- higher tiers can include rate-limit and enterprise-style options
The current pricing context for Tavily is:
| Plan | Price | API Credits | Notes |
|---|---|---|---|
| Researcher | $0/mo | 1,000/mo | Free tier for new creators, no credit card required |
| Pay As You Go | $0.008/credit | usage-based | Flexible usage, cancel anytime |
| Project | $30/mo | 4,000/mo | Higher rate limits for enthusiasts |
| Enterprise | Custom | Custom | Custom rate limits, enterprise support, security/privacy |
Practical usage notes from the current Tavily pricing page:
- credits are used across Tavily capabilities such as search, extract, and crawl
- monthly included credits reset on the first day of the month
- if credits run out, requests stop until credits reset or the plan changes
- the API key is obtained from the Tavily dashboard after sign-up
- the pricing page also mentions a student offering and email support on non-enterprise plans
The current pricing context for Exa is:
| Product / Plan Surface | Price | Usage Basis | Notes |
|---|---|---|---|
| Free usage | $0/mo | up to 1,000 requests/month | Entry-level free API usage |
| Search | $7 / 1k requests | 1–10 results | +$1 / 1k additional results beyond 10 |
| Agentic Search | $12 / 1k requests | request-based | deeper structured search mode |
| Agentic Search + reasoning | +$3 / 1k requests | add-on | applied on top of Agentic Search reasoning usage |
| Contents | $1 / 1k pages per content type | page-based | useful for full-page retrieval |
| Answer | $5 / 1k answers | answer-based | direct answer generation with citations |
| Enterprise | Custom | custom | custom QPS, rate limits, support, pricing |
Practical usage notes from current Exa docs/pricing:
- Search is positioned for agent web-search tool calls
- Search latency is described around 100–1200 ms depending on search mode
- Agentic Search is slower (roughly 4–30 s) but intended for deeper structured workflows
- Contents is presented as a token-efficient full-page retrieval path
- Enterprise mentions custom QPS, custom rate limits, and volume discounts
- Startup and education grants are mentioned with free credit programs
These tables are current planning notes, not permanent contracts. The implementation may change later if pricing, reliability, or capability trade-offs make another provider a better fit.
| Provider | Best fit in this repo | Search | Extract / Scrape | Fallback role | Cost profile | Notes |
|---|---|---|---|---|---|---|
| WebSearchAPI.ai | balanced default for current WebSearch + one current WebFetch extraction backend | Yes | Yes | interchangeable search/extraction backend | predictable monthly plans | one provider can cover both search and extraction paths |
| Tavily | strong search provider for Claude Code custom-endpoint workflows | Yes | Yes (Extract) |
interchangeable search/extraction backend | lower entry cost and clear PAYG path | now supported in both search and WebFetch extraction flow |
| Exa | current additional provider with stronger retrieval-oriented content features | Yes | Yes (Contents) |
interchangeable search/extraction backend | request-based pricing; deeper modes cost more | supported in both search and WebFetch extraction flow |
- WebSearchAPI.ai is currently the broadest fit for the project because the current implementation already uses it for both WebSearch substitution and WebFetch scraper fallback.
- Tavily is currently the most practical second search provider because its Search and Extract APIs are clearly separated and its pricing/entry path are straightforward.
- Exa is now an active search-layer provider in this repo and remains strategically interesting because it adds another provider-backed search path without changing the existing abstraction model.
Exa is now integrated into the WebSearch provider layer of this project.
Searchendpoint for web search results and their contentsAgentic Search/ deeper search modes for structured/deeper search workflowsContentsendpoint for page-content retrievalAnswerendpoint for direct answers with citationsResearchproduct for autonomous research tasks
- Exa is a current WebSearch provider
- Exa participates through the shared search-provider abstraction and policy layer
- Exa Contents is also part of the current WebFetch extraction backend set
- in WebFetch, Exa is one interchangeable extraction backend among the three supported providers
- it has a search-first API surface that maps naturally to the project’s provider-abstraction direction
- it has dedicated content retrieval pricing (
Contents) that could matter later if the project expands beyond pure search substitution - docs show explicit search parameters like result count, domains, country, and content inclusion, which fits the current hook model well
- Free usage: up to 1,000 requests/month
- Search: $7 / 1k requests for 1–10 results, +$1 / 1k additional results beyond 10
- Agentic Search: $12 / 1k requests
- Agentic Search with reasoning: +$3 / 1k requests on top of that mode
- Contents: $1 / 1k pages per content type
- Answer: $5 / 1k answers
- Research: priced separately around agent-style operations / page reads / reasoning-token usage
- Exa is already integrated as a current search-layer provider and not used as a WebFetch replacement.
- The clean path remains:
- keep Exa behind the shared search-provider policy
- keep WebFetch extractor work separate unless there is a deliberate extract-provider expansion phase
- Exa should continue to use the same provider abstraction as Tavily and WebSearchAPI.ai.
This project uses a fully permissive fallback policy.
If the custom path cannot complete successfully, it should not trap the user when the native Claude Code tool can still continue.
Current behavior:
- success in
fallbackmode → return the first successful provider result in effective provider order - success in
parallelmode → return all successful provider results - partial failure in
parallelmode → still return successful provider results and list failed providers - no provider keys → allow native WebSearch
- auth failure → allow native WebSearch
- credit / quota failure → allow native WebSearch
- transient provider failure → allow native WebSearch
- unknown provider failure → allow native WebSearch
Current provider policy modes:
fallbackparallel(current default)
Current default order:
tavilywebsearchapi
Provider-selection relationship:
CLAUDE_WEB_HOOKS_SEARCH_MODEcontrols how providers are executedCLAUDE_WEB_HOOKS_SEARCH_PROVIDERScontrols the configured provider orderCLAUDE_WEB_HOOKS_SEARCH_PRIMARY, if set, is treated as a priority override and moved to the front of the provider order- in
fallbackmode, that means the system starts withSEARCH_PRIMARYfirst, then continues through the remaining ordered providers - in
parallelmode, all providers still run, but the success/failure blocks are ordered with the effective provider order
Current behavior:
- fetch-readable page → allow native WebFetch
- unsupported/probe-unusable → allow native WebFetch
- if extraction is recommended, the hook supports three interchangeable extraction backends:
- WebSearchAPI.ai Scrape
- Tavily Extract
- Exa Contents
- one backend is chosen per request
- if
CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_PRIMARYis set, it is tried first - if
PRIMARYis not set, the initial backend is chosen randomly from configured providers that have keys available
- if
- if the chosen backend fails, the hook rotates through the remaining configured providers in fallback order
- if all extraction backends fail (including exhausted key pools), allow native WebFetch
- on success, return exactly one extracted content result from the winning backend
Failure classification is shared by both hooks:
hooks/shared/failure-policy.cjs
Current classes:
auth-failedcredit-or-quota-failedtransient-provider-failedunknown
In the current version, all four classes allow native fallback.
flowchart TD
A[Claude Code tool event] --> B[PreToolUse hook]
B --> C{Should custom path take over?}
C -->|No| D[Allow native Claude Code tool]
C -->|Yes| E[Execute custom path]
E --> F{Custom path succeeded?}
F -->|Yes| G[Return custom result]
F -->|No| D
claude-code-web-hooks/
README.md
LICENSE
.gitignore
design.md
changelog.md
TODO.md
settings.example.json
apikey.example.json
apikeys.example.txt
install.sh
uninstall.sh
verify.sh
fixtures/
article-readable.html
template-heavy.html
browser-shell.html
hooks/
websearch-custom.cjs
webfetch-scraper.cjs
shared/
failure-policy.cjs
provider-config.cjs
search-provider-contract.cjs
search-provider-policy.cjs
extract-provider-contract.cjs
extract-provider-policy.cjs
search-providers/
websearchapi.cjs
tavily.cjs
exa.cjs
extract-providers/
websearchapi.cjs
tavily.cjs
exa.cjs
git clone <your-repo-url>
cd claude-code-web-hooks
./install.shWhat install.sh does:
- copies hook files into
~/.claude/hooks/ - copies the shared helpers into
~/.claude/hooks/shared/ - copies the search provider adapters into
~/.claude/hooks/shared/search-providers/ - copies the extraction provider adapters into
~/.claude/hooks/shared/extract-providers/ - backs up
~/.claude/settings.jsonbefore editing - merges the required
PreToolUse -> WebSearch - merges the required
PreToolUse -> WebFetch - preserves unrelated Claude Code settings
After install:
- open
/hooksin Claude Code to reload configuration - or restart the Claude Code session
- Copy the hook files into
~/.claude/hooks/ - Copy the shared helpers into
~/.claude/hooks/shared/ - Copy the search provider adapters into
~/.claude/hooks/shared/search-providers/ - Copy the extraction provider adapters into
~/.claude/hooks/shared/extract-providers/ - Merge the
hooksblock fromsettings.example.jsoninto~/.claude/settings.json - Add the env variables you want to use (
WEBSEARCHAPI_API_KEY,TAVILY_API_KEY, and/orEXA_API_KEY)
./uninstall.shWhat uninstall.sh does:
- removes the installed hook files from
~/.claude/hooks/ - removes the installed shared helpers from
~/.claude/hooks/shared/ - removes the installed search provider adapters from
~/.claude/hooks/shared/search-providers/ - removes the installed extraction provider adapters from
~/.claude/hooks/shared/extract-providers/ - backs up
~/.claude/settings.jsonbefore editing - removes only the
PreToolUse -> WebSearchandPreToolUse -> WebFetchentries installed by this project - leaves unrelated Claude Code settings intact
Use settings.example.json as the base example.
The hook commands should point to the real installed paths under ~/.claude/hooks/ after running install.sh.
The project currently uses separate provider keys:
WEBSEARCHAPI_API_KEYfor WebSearchAPI.ai Search + WebSearchAPI.ai ScrapeTAVILY_API_KEYfor Tavily Search + Tavily ExtractEXA_API_KEYfor Exa Search + Exa Contents
WEBSEARCHAPI_API_KEY supports:
"WEBSEARCHAPI_API_KEY": "your_api_key""WEBSEARCHAPI_API_KEY": "key1|key2|key3""WEBSEARCHAPI_API_KEY": "/absolute/path/to/apikey.json"Example file:
["apikey1", "apikey2"]"WEBSEARCHAPI_API_KEY": "/absolute/path/to/apikeys.txt"Example file:
# One API key per line
# Lines starting with # are ignored
apikey1
apikey2
Notes:
- file mode first tries JSON-array parsing
- if JSON parsing fails, it falls back to newline-separated parsing
- blank lines are ignored
- lines starting with
#are ignored - inline pools and file pools are shuffled per request
- if one key fails, the next key is tried automatically
TAVILY_API_KEY and EXA_API_KEY follow the same input rules:
- single inline key
- inline pool using
| - JSON array file path
- newline-separated text file path
Recommended example for the current implementation:
Important:
- WebFetch supports three interchangeable extraction backends:
websearchapitavilyexa- if
CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_PRIMARYis set, it is tried first- if
PRIMARYis not set, the first extraction backend is chosen randomly from providers that have keys available- fallback then continues through the remaining available providers
- current default search behavior in code is still:
CLAUDE_WEB_HOOKS_SEARCH_MODE=parallelCLAUDE_WEB_HOOKS_SEARCH_PROVIDERS=tavily,websearchapi
{
"env": {
"CLAUDE_WEB_HOOKS_SEARCH_MODE": "parallel",
"CLAUDE_WEB_HOOKS_SEARCH_PROVIDERS": "tavily,websearchapi",
"WEBSEARCHAPI_API_KEY": "/absolute/path/to/apikeys.txt",
"TAVILY_API_KEY": "/absolute/path/to/tavily-keys.txt",
"EXA_API_KEY": "/absolute/path/to/exa-keys.txt",
"WEBSEARCHAPI_MAX_RESULTS": "10",
"WEBSEARCHAPI_INCLUDE_CONTENT": "1",
"WEBSEARCHAPI_COUNTRY": "us",
"WEBSEARCHAPI_LANGUAGE": "en",
"TAVILY_SEARCH_DEPTH": "advanced",
"TAVILY_MAX_RESULTS": "10",
"TAVILY_TOPIC": "general",
"EXA_SEARCH_TYPE": "auto",
"EXA_MAX_RESULTS": "10",
"EXA_CATEGORY": "news",
"CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_MODE": "fallback",
"CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_PROVIDERS": "websearchapi,tavily,exa",
"CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_TIMEOUT": "25",
"CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_FORMAT": "markdown",
"WEBSEARCHAPI_SCRAPE_ENGINE": "browser",
"TAVILY_EXTRACT_DEPTH": "advanced",
"EXA_CONTENTS_VERBOSITY": "standard",
"CLAUDE_WEB_HOOKS_SEARCH_TIMEOUT": "55",
"TAVILY_SEARCH_TIMEOUT": "55",
"EXA_SEARCH_TIMEOUT": "55",
"WEBFETCH_PROBE_TIMEOUT": "12",
"WEBFETCH_PROBE_MAX_HTML_BYTES": "262144",
"WEBFETCH_SCRAPER_TIMEOUT": "25",
"CLAUDE_WEB_HOOKS_DEBUG": "0"
}
}What these keys do:
CLAUDE_WEB_HOOKS_SEARCH_MODE: search provider execution mode (fallbackorparallel)CLAUDE_WEB_HOOKS_SEARCH_PROVIDERS: provider order for the search policy layerCLAUDE_WEB_HOOKS_SEARCH_PRIMARY: optional priority override that moves one search provider to the front of the ordered listCLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_MODE: extraction backend execution mode (fallbackonly)CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_PROVIDERS: ordered extraction providers for WebFetch (websearchapi,tavily,exa)CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_PRIMARY: optional preferred extraction backend to try first- if
PRIMARYis not set, the hook randomly chooses the first backend from configured providers that currently have keys available CLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_TIMEOUT: shared extraction timeoutCLAUDE_WEB_HOOKS_WEBFETCH_EXTRACT_FORMAT: preferred extraction output formatWEBSEARCHAPI_API_KEY: WebSearchAPI.ai key / key pool / key file pathTAVILY_API_KEY: Tavily key / key pool / key file pathEXA_API_KEY: Exa key / key pool / key file pathTAVILY_SEARCH_DEPTH,TAVILY_MAX_RESULTS,TAVILY_TOPIC: Tavily Search tuningEXA_SEARCH_TYPE,EXA_MAX_RESULTS,EXA_CATEGORY: Exa Search tuningWEBSEARCHAPI_SCRAPE_ENGINE: WebSearchAPI.ai Scrape tuningTAVILY_EXTRACT_DEPTH: Tavily Extract tuningEXA_CONTENTS_VERBOSITY: Exa Contents text verbosity tuningWEBFETCH_PROBE_TIMEOUT,WEBFETCH_PROBE_MAX_HTML_BYTES: initial HTML probe tuning for WebFetch detectionWEBFETCH_SCRAPER_TIMEOUT: legacy shared scraper timeout alias for backward compatibilityCLAUDE_WEB_HOOKS_SEARCH_TIMEOUT: shared default timeout for search providersTAVILY_SEARCH_TIMEOUT,EXA_SEARCH_TIMEOUT: search provider-specific timeout overridesCLAUDE_WEB_HOOKS_DEBUG: debug logging for the hook layer
Step-by-step behavior:
- Build the effective provider order
- start from
CLAUDE_WEB_HOOKS_SEARCH_PROVIDERS - if
CLAUDE_WEB_HOOKS_SEARCH_PRIMARYis set, move it to the front
- start from
- Run the first provider in that order
- If it succeeds, return that provider’s result immediately
- If it fails, try the next provider
- If every configured provider fails, allow native Claude Code WebSearch to continue
Characteristics:
- best when you want lower cost and fewer provider calls
- easiest mode to reason about if you want a clear primary → secondary → native flow
Step-by-step behavior:
- Build the effective provider order
- start from
CLAUDE_WEB_HOOKS_SEARCH_PROVIDERS - if
CLAUDE_WEB_HOOKS_SEARCH_PRIMARYis set, move it to the front for display/priority ordering
- start from
- Run all configured providers concurrently
- Collect every successful provider result
- Collect every provider failure separately
- If one or more providers succeed:
- return all successful provider results
- also show which providers failed
- If all configured providers fail:
- allow native Claude Code WebSearch to continue
Characteristics:
- best when you want maximum resilience and visibility across providers
- costs more because more than one provider can be called on each search
- output can be longer because multiple successful result blocks may be returned
{
"env": {
"CLAUDE_WEB_HOOKS_SEARCH_MODE": "fallback",
"CLAUDE_WEB_HOOKS_SEARCH_PROVIDERS": "tavily,websearchapi"
}
}{
"env": {
"CLAUDE_WEB_HOOKS_SEARCH_MODE": "fallback",
"CLAUDE_WEB_HOOKS_SEARCH_PROVIDERS": "tavily,websearchapi",
"CLAUDE_WEB_HOOKS_SEARCH_PRIMARY": "websearchapi"
}
}Effective order becomes:
websearchapitavily
{
"env": {
"CLAUDE_WEB_HOOKS_SEARCH_MODE": "parallel",
"CLAUDE_WEB_HOOKS_SEARCH_PROVIDERS": "tavily,websearchapi"
}
}{
"hooks": {
"PreToolUse": [
{
"matcher": "WebSearch",
"hooks": [
{
"type": "command",
"command": "node \"/home/your-user/.claude/hooks/websearch-custom.cjs\"",
"timeout": 120
}
]
},
{
"matcher": "WebFetch",
"hooks": [
{
"type": "command",
"command": "node \"/home/your-user/.claude/hooks/webfetch-scraper.cjs\"",
"timeout": 120
}
]
}
]
}
}See also:
settings.example.json
Run:
./verify.shWhat it checks:
- hook syntax
- install/uninstall script syntax
- example settings shape
- fixture-based WebFetch classification
- shared failure policy behavior
- search provider policy availability
- WebFetch extraction provider policy selection/fallback behavior
- parallel-mode aggregation sanity
This project is currently suitable for:
- private repository use
- internal distribution
- controlled public release after reviewing repository metadata and installation instructions
design.md— design direction and contractschangelog.md— historyTODO.md— release and follow-up worksettings.example.json— Claude Code settings example